appwrite/src/Appwrite/Migration/Migration.php

445 lines
12 KiB
PHP
Raw Normal View History

2021-01-13 16:51:02 +00:00
<?php
namespace Appwrite\Migration;
2024-03-06 17:34:21 +00:00
use Exception;
use Utopia\CLI\Console;
2022-01-18 11:05:04 +00:00
use Utopia\Config\Config;
2024-03-06 17:34:21 +00:00
use Utopia\Database\Database;
use Utopia\Database\Document;
2025-05-16 05:24:11 +00:00
use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Limit;
use Utopia\Database\Exception\Structure;
2022-12-14 15:42:25 +00:00
use Utopia\Database\Helpers\ID;
use Utopia\Database\PDO;
2022-05-13 12:49:31 +00:00
use Utopia\Database\Validator\Authorization;
2021-01-13 16:51:02 +00:00
abstract class Migration
{
protected int $limit = 100;
2021-12-08 17:50:04 +00:00
2022-01-18 11:05:04 +00:00
protected Document $project;
2025-05-16 05:24:11 +00:00
protected Database $dbForProject;
2021-01-18 14:43:55 +00:00
2025-05-16 05:24:11 +00:00
protected Database $dbForPlatform;
2025-05-16 13:39:25 +00:00
/**
* @var callable(Document): Database
*/
protected mixed $getProjectDB;
protected PDO $pdo;
2023-02-15 07:35:24 +00:00
2021-07-02 09:09:02 +00:00
/**
2025-05-16 05:24:11 +00:00
* @var array<string, string>
2021-07-02 09:09:02 +00:00
*/
public static array $versions = [
2022-09-14 07:12:17 +00:00
'1.0.0-RC1' => 'V15',
2022-09-14 19:03:00 +00:00
'1.0.0' => 'V15',
'1.0.1' => 'V15',
2022-11-14 21:42:48 +00:00
'1.0.3' => 'V15',
2022-11-15 12:59:35 +00:00
'1.1.0' => 'V16',
2022-11-17 13:38:49 +00:00
'1.1.1' => 'V16',
2022-11-23 17:43:48 +00:00
'1.1.2' => 'V16',
2022-12-07 11:01:58 +00:00
'1.2.0' => 'V17',
2023-02-13 23:55:54 +00:00
'1.2.1' => 'V17',
2023-02-15 07:35:24 +00:00
'1.3.0' => 'V18',
2023-04-12 16:13:49 +00:00
'1.3.1' => 'V18',
2023-04-24 11:10:28 +00:00
'1.3.2' => 'V18',
2023-04-28 19:29:46 +00:00
'1.3.3' => 'V18',
2023-05-03 20:39:05 +00:00
'1.3.4' => 'V18',
2023-05-30 15:45:29 +00:00
'1.3.5' => 'V18',
2023-06-02 12:26:51 +00:00
'1.3.6' => 'V18',
2023-06-03 14:41:01 +00:00
'1.3.7' => 'V18',
2023-07-18 22:21:58 +00:00
'1.3.8' => 'V18',
2023-06-11 07:59:44 +00:00
'1.4.0' => 'V19',
2023-08-30 20:05:51 +00:00
'1.4.1' => 'V19',
2023-09-06 18:22:04 +00:00
'1.4.2' => 'V19',
2023-09-14 19:53:32 +00:00
'1.4.3' => 'V19',
2023-09-27 21:28:28 +00:00
'1.4.4' => 'V19',
2023-10-09 23:37:06 +00:00
'1.4.5' => 'V19',
2023-10-18 23:07:47 +00:00
'1.4.6' => 'V19',
2023-10-23 18:53:40 +00:00
'1.4.7' => 'V19',
2023-10-27 17:01:38 +00:00
'1.4.8' => 'V19',
2023-10-31 19:40:45 +00:00
'1.4.9' => 'V19',
2023-11-10 00:25:28 +00:00
'1.4.10' => 'V19',
'1.4.11' => 'V19',
2023-11-17 19:51:25 +00:00
'1.4.12' => 'V19',
'1.4.13' => 'V19',
2024-02-01 10:21:50 +00:00
'1.5.0' => 'V20',
2024-03-08 16:25:10 +00:00
'1.5.1' => 'V20',
2024-03-08 20:55:25 +00:00
'1.5.2' => 'V20',
2024-03-11 16:59:12 +00:00
'1.5.3' => 'V20',
2024-03-13 10:08:50 +00:00
'1.5.4' => 'V20',
2024-04-24 21:28:26 +00:00
'1.5.5' => 'V20',
2024-05-16 23:46:05 +00:00
'1.5.6' => 'V20',
2024-05-24 09:35:57 +00:00
'1.5.7' => 'V20',
2024-06-25 04:04:14 +00:00
'1.5.8' => 'V20',
2024-08-09 15:57:07 +00:00
'1.5.9' => 'V20',
2024-09-18 03:21:45 +00:00
'1.5.10' => 'V20',
'1.5.11' => 'V20',
2024-09-09 11:00:22 +00:00
'1.6.0' => 'V21',
2024-11-22 22:47:02 +00:00
'1.6.1' => 'V21',
'1.6.2' => 'V21',
'1.7.0-RC1' => 'V22',
'1.7.0' => 'V22',
2025-05-19 23:21:31 +00:00
'1.7.1' => 'V22',
'1.7.2' => 'V22',
2021-07-02 09:09:02 +00:00
];
2022-02-17 18:24:50 +00:00
/**
2025-05-16 05:24:11 +00:00
* @var array<string, array<string, mixed>>
2022-02-17 18:24:50 +00:00
*/
protected array $collections;
2024-10-08 07:54:40 +00:00
public function __construct()
2022-02-17 18:24:50 +00:00
{
2024-10-08 07:54:40 +00:00
Authorization::disable();
Authorization::setDefaultStatus(false);
2022-09-07 08:43:05 +00:00
$this->collections = Config::getParam('collections', []);
2025-05-16 10:06:37 +00:00
$this->collections['projects']['_metadata'] = [
'$id' => ID::custom('_metadata'),
'$collection' => Database::METADATA,
2025-05-16 05:24:11 +00:00
];
2025-05-16 10:06:37 +00:00
$this->collections['projects']['audit'] = [
'$id' => ID::custom('audit'),
'$collection' => Database::METADATA,
2025-05-16 05:24:11 +00:00
];
2022-02-17 18:24:50 +00:00
}
/**
* Set project for migration.
*
2022-01-18 11:05:04 +00:00
* @param Document $project
2025-05-16 05:24:11 +00:00
* @param Database $dbForProject
* @param Database $dbForPlatform
2021-12-08 17:50:04 +00:00
* @return self
*/
2025-05-16 13:39:25 +00:00
public function setProject(
Document $project,
Database $dbForProject,
Database $dbForPlatform,
?callable $getProjectDB = null
2025-05-16 13:57:57 +00:00
): self {
$this->project = $project;
2025-05-16 05:24:11 +00:00
$this->dbForProject = $dbForProject;
$this->dbForPlatform = $dbForPlatform;
2025-05-16 13:39:25 +00:00
$this->getProjectDB = $getProjectDB;
2021-12-08 17:50:04 +00:00
return $this;
}
2023-04-24 11:00:23 +00:00
/**
* Set PDO for Migration.
*
* @param PDO $pdo
2025-05-16 05:24:11 +00:00
* @return Migration
2023-04-24 11:00:23 +00:00
*/
public function setPDO(PDO $pdo): self
2023-02-15 07:35:24 +00:00
{
$this->pdo = $pdo;
return $this;
}
/**
* Iterates through every document.
*
2021-01-18 14:43:55 +00:00
* @param callable $callback
2025-05-16 05:24:11 +00:00
* @throws Exception
*/
2021-01-21 09:57:15 +00:00
public function forEachDocument(callable $callback): void
{
2025-05-23 17:39:56 +00:00
$projectInternalId = $this->project->getInternalId();
2025-05-16 05:24:11 +00:00
$collections = match ($projectInternalId) {
'console' => $this->collections['console'],
default => $this->collections['projects'],
};
foreach ($collections as $collection) {
2025-05-16 05:24:11 +00:00
// Only migrate top-level collections
2022-05-23 14:54:50 +00:00
if ($collection['$collection'] !== Database::METADATA) {
2022-06-22 13:52:21 +00:00
continue;
2022-05-23 14:54:50 +00:00
}
2022-06-27 16:22:29 +00:00
2025-05-16 10:06:59 +00:00
Console::log('Migrating documents for collection "' . $collection['$id'] . '"');
2022-01-18 11:05:04 +00:00
2025-05-16 13:57:57 +00:00
$this->dbForProject->foreach($collection['$id'], function (Document $document) use ($collection, $callback) {
if (empty($document->getId()) || empty($document->getCollection())) {
2025-05-16 05:24:11 +00:00
return;
}
$old = $document->getArrayCopy();
2025-05-16 05:24:11 +00:00
$new = $callback($document);
2025-05-16 05:24:11 +00:00
if ($new === null || $new->getArrayCopy() == $old) {
return;
}
try {
2025-05-16 05:24:11 +00:00
$this->dbForProject->updateDocument(
$document->getCollection(),
$document->getId(),
$document
);
} catch (\Throwable $th) {
2025-05-16 05:24:11 +00:00
Console::error("Failed to update document \"{$document->getId()}\" in collection \"{$collection['$id']}\":" . $th->getMessage());
return;
}
2025-05-16 05:24:11 +00:00
});
}
}
2022-05-13 12:49:31 +00:00
/**
* Creates collection from the config collection.
2022-05-13 12:49:31 +00:00
*
* @param string $id
* @param string|null $name
* @return void
* @throws \Throwable
*/
protected function createCollection(string $id, string $name = null): void
{
$name ??= $id;
2025-05-23 17:39:56 +00:00
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
2025-05-16 05:24:11 +00:00
if (!$this->dbForProject->getCollection($id)->isEmpty()) {
2025-05-16 12:18:57 +00:00
return;
}
2025-05-16 10:06:59 +00:00
2025-05-16 12:18:57 +00:00
$collection = $this->collections[$collectionType][$id];
2025-05-16 10:06:59 +00:00
2025-05-16 12:18:57 +00:00
$attributes = [];
foreach ($collection['attributes'] as $attribute) {
$attributes[] = new Document($attribute);
}
2022-05-13 12:49:31 +00:00
2025-05-16 12:18:57 +00:00
$indexes = [];
foreach ($collection['indexes'] as $index) {
$indexes[] = new Document($index);
}
try {
$this->dbForProject->createCollection($name, $attributes, $indexes);
2025-05-16 13:57:57 +00:00
} catch (Duplicate) {
2025-05-16 12:18:57 +00:00
Console::warning('Failed to create collection "' . $name . '": Collection already exists');
2025-05-16 05:24:11 +00:00
}
}
/**
* Creates attributes from collections.php
*
* @param Database $database
* @param string $collectionId
* @param array $attributeIds
* @param string|null $from
* @return void
* @throws \Utopia\Database\Exception
* @throws \Utopia\Database\Exception\Authorization
* @throws Conflict
* @throws Duplicate
* @throws Limit
* @throws Structure
*/
public function createAttributesFromCollection(
Database $database,
string $collectionId,
array $attributeIds,
string $from = null
2025-05-16 13:57:57 +00:00
): void {
2025-05-16 05:24:11 +00:00
$from ??= $collectionId;
2025-05-23 17:39:56 +00:00
$collectionType = match ($this->project->getInternalId()) {
2025-05-16 05:24:11 +00:00
'console' => 'console',
default => 'projects',
};
if ($from === 'files') {
$collectionType = 'buckets';
}
$collection = $this->collections[$collectionType][$from] ?? null;
if ($collection === null) {
throw new Exception("Collection {$from} not found");
}
2025-05-16 08:30:16 +00:00
$attributesToCreate = [];
$attributes = $collection['attributes'];
$attributeKeys = \array_column($collection['attributes'], '$id');
2025-05-16 05:24:11 +00:00
foreach ($attributeIds as $attributeId) {
2025-05-16 08:30:16 +00:00
$attributeKey = \array_search($attributeId, $attributeKeys);
2025-05-16 05:24:11 +00:00
2025-05-16 08:30:16 +00:00
if ($attributeKey === false) {
2025-05-16 05:24:11 +00:00
throw new Exception("Attribute {$attributeId} not found");
2022-05-13 12:49:31 +00:00
}
2025-05-16 05:24:11 +00:00
2025-05-16 08:30:16 +00:00
$attribute = $attributes[$attributeKey];
2025-05-16 05:24:11 +00:00
$attribute['filters'] ??= [];
$attribute['default'] ??= null;
$attribute['default'] = \in_array('json', $attribute['filters'])
? \json_encode($attribute['default'])
: $attribute['default'];
2025-05-16 08:30:16 +00:00
$attributesToCreate[] = $attribute;
2022-05-13 12:49:31 +00:00
}
2025-05-16 05:24:11 +00:00
$database->createAttributes(
collection: $collectionId,
2025-05-16 08:30:16 +00:00
attributes: $attributesToCreate,
2025-05-16 05:24:11 +00:00
);
2022-05-13 12:49:31 +00:00
}
2022-06-21 10:17:18 +00:00
/**
* Creates attribute from collections.php
*
2025-05-16 05:24:11 +00:00
* @param Database $database
2022-06-21 10:17:18 +00:00
* @param string $collectionId
* @param string $attributeId
2025-05-16 05:24:11 +00:00
* @param string|null $from
2022-06-21 10:17:18 +00:00
* @return void
2025-05-16 05:24:11 +00:00
* @throws \Utopia\Database\Exception
* @throws \Utopia\Database\Exception\Authorization
* @throws Conflict
* @throws Duplicate
* @throws Limit
* @throws Structure
2022-06-21 10:17:18 +00:00
*/
2025-05-16 05:24:11 +00:00
public function createAttributeFromCollection(
Database $database,
string $collectionId,
string $attributeId,
string $from = null
2025-05-16 13:57:57 +00:00
): void {
2022-06-22 13:52:21 +00:00
$from ??= $collectionId;
2023-08-22 23:14:23 +00:00
2025-05-23 17:39:56 +00:00
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
2023-08-22 23:14:23 +00:00
if ($from === 'files') {
$collectionType = 'buckets';
}
2023-08-22 23:14:23 +00:00
$collection = $this->collections[$collectionType][$from] ?? null;
2023-08-22 23:14:23 +00:00
2025-05-16 05:24:11 +00:00
if ($collection === null) {
throw new Exception("Collection {$from} not found");
2022-06-21 10:17:18 +00:00
}
2023-08-22 23:14:23 +00:00
2022-06-21 10:17:18 +00:00
$attributes = $collection['attributes'];
2025-05-16 05:24:11 +00:00
$attributeKey = \array_search($attributeId, \array_column($attributes, '$id'));
2022-06-21 10:17:18 +00:00
2022-06-21 13:46:05 +00:00
if ($attributeKey === false) {
2022-06-21 10:17:18 +00:00
throw new Exception("Attribute {$attributeId} not found");
}
$attribute = $attributes[$attributeKey];
2022-09-08 16:46:18 +00:00
$filters = $attribute['filters'] ?? [];
$default = $attribute['default'] ?? null;
2022-06-21 10:17:18 +00:00
$database->createAttribute(
collection: $collectionId,
id: $attributeId,
type: $attribute['type'],
size: $attribute['size'],
2025-05-16 05:24:11 +00:00
required: $attribute['required'],
default: \in_array('json', $filters) ? \json_encode($default) : $default,
signed: $attribute['signed'] ?? true,
2022-06-21 10:17:18 +00:00
array: $attribute['array'] ?? false,
format: $attribute['format'] ?? '',
formatOptions: $attribute['formatOptions'] ?? [],
2022-09-08 16:46:18 +00:00
filters: $filters,
2022-06-21 10:17:18 +00:00
);
}
/**
* Creates index from collections.php
*
2025-05-16 05:24:11 +00:00
* @param Database $database
2022-06-21 10:17:18 +00:00
* @param string $collectionId
* @param string $indexId
2022-09-08 16:46:18 +00:00
* @param string|null $from
2022-06-21 10:17:18 +00:00
* @return void
* @throws \Exception
2025-05-16 05:24:11 +00:00
* @throws Duplicate
* @throws Limit
2022-06-21 10:17:18 +00:00
*/
2022-09-08 16:46:18 +00:00
public function createIndexFromCollection(Database $database, string $collectionId, string $indexId, string $from = null): void
2022-06-21 10:17:18 +00:00
{
2022-09-08 16:46:18 +00:00
$from ??= $collectionId;
2023-08-22 23:14:23 +00:00
2025-05-23 17:39:56 +00:00
$collectionType = match ($this->project->getInternalId()) {
'console' => 'console',
default => 'projects',
};
2023-08-22 23:14:23 +00:00
if ($from === 'files') {
$collectionType = 'buckets';
}
$collection = $this->collections[$collectionType][$from] ?? null;
2022-06-21 10:17:18 +00:00
2025-05-16 05:24:11 +00:00
if ($collection === null) {
2022-06-21 10:17:18 +00:00
throw new Exception("Collection {$collectionId} not found");
}
2023-08-22 23:14:23 +00:00
2022-06-21 10:17:18 +00:00
$indexes = $collection['indexes'];
2025-05-16 05:24:11 +00:00
$indexKey = \array_search($indexId, \array_column($indexes, '$id'));
2022-06-21 10:17:18 +00:00
if ($indexKey === false) {
2023-08-22 00:12:10 +00:00
throw new Exception("Index {$indexId} not found");
2022-06-21 10:17:18 +00:00
}
$index = $indexes[$indexKey];
$database->createIndex(
collection: $collectionId,
id: $indexId,
type: $index['type'],
attributes: $index['attributes'],
lengths: $index['lengths'] ?? [],
orders: $index['orders'] ?? []
);
}
2023-02-15 07:35:24 +00:00
/**
* Change a collection attribute's internal type
*
* @param string $collection
* @param string $attribute
* @param string $type
* @return void
2025-05-16 05:24:11 +00:00
* @throws \Utopia\Database\Exception
2023-02-15 07:35:24 +00:00
*/
protected function changeAttributeInternalType(string $collection, string $attribute, string $type): void
{
2025-05-23 17:39:56 +00:00
$stmt = $this->pdo->prepare("ALTER TABLE `{$this->dbForProject->getDatabase()}`.`_{$this->project->getInternalId()}_{$collection}` MODIFY `$attribute` $type;");
2023-02-15 07:35:24 +00:00
try {
$stmt->execute();
} catch (\Throwable $e) {
2023-02-15 07:35:24 +00:00
Console::warning($e->getMessage());
}
}
/**
* Executes migration for set project.
*/
abstract public function execute(): void;
2021-01-13 16:51:02 +00:00
}