diff --git a/.gitignore b/.gitignore index 86cf51814..95d288b53 100755 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ loader.php /bin/view/results/ .vscode .vscode/* +database.sql ## - Oh Wess! Makefile diff --git a/composer.lock b/composer.lock index db2620b32..dc7bdeb79 100644 --- a/composer.lock +++ b/composer.lock @@ -336,16 +336,16 @@ }, { "name": "utopia-php/framework", - "version": "0.22.1", + "version": "0.23.1", "source": { "type": "git", "url": "https://github.com/utopia-php/framework.git", - "reference": "9f35d36ed4b8fa1c92962c77ef02b49c2f5919df" + "reference": "d595df075aa9ee46147a388c63064b03aeeac466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/framework/zipball/9f35d36ed4b8fa1c92962c77ef02b49c2f5919df", - "reference": "9f35d36ed4b8fa1c92962c77ef02b49c2f5919df", + "url": "https://api.github.com/repos/utopia-php/framework/zipball/d595df075aa9ee46147a388c63064b03aeeac466", + "reference": "d595df075aa9ee46147a388c63064b03aeeac466", "shasum": "" }, "require": { @@ -365,12 +365,6 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - } - ], "description": "A simple, light and advanced PHP framework", "keywords": [ "framework", @@ -379,9 +373,9 @@ ], "support": { "issues": "https://github.com/utopia-php/framework/issues", - "source": "https://github.com/utopia-php/framework/tree/0.22.1" + "source": "https://github.com/utopia-php/framework/tree/0.23.1" }, - "time": "2022-10-07T14:51:40+00:00" + "time": "2022-10-19T10:35:44+00:00" } ], "packages-dev": [ @@ -4114,5 +4108,5 @@ "ext-mongodb": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/phpunit.xml b/phpunit.xml index 7bc4228a4..97f6e15b5 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/ diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 3ad69136b..aae342558 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -257,7 +257,7 @@ abstract public function renameIndex(string $collection, string $old, string $ne * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes Document[] * @param array $lengths * @param array $orders * @@ -476,9 +476,9 @@ abstract public function getKeywords(): array; /** * Filter Keys - * - * @throws Exception + * @param string $value * @return string + * @throws Exception */ public function filter(string $value): string { @@ -490,4 +490,26 @@ public function filter(string $value): string return $value; } + + + /** + * Returns attribute from List of Document $attributes[] + * @param string $attributeKey + * @param array $attributes Document[] + * returns Document + * @return Document + * @throws Exception + */ + protected function findAttributeInList(string $attributeKey, array $attributes): Document + { + /** @var Document $attribute */ + foreach ($attributes as $attribute){ + if($this->filter($attributeKey) === $this->filter($attribute->getId())){ + return $attribute; + } + } + + throw new \Exception('Attribute ' . $attributeKey . ' not found'); + } + } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index edf177376..fb7714404 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -108,7 +108,7 @@ public function exists(string $database, ?string $collection): bool /** * List Databases - * + * * @return array */ public function list(): array @@ -148,37 +148,56 @@ public function createCollection(string $name, array $attributes = [], array $in $database = $this->getDefaultDatabase(); $namespace = $this->getNamespace(); $id = $this->filter($name); + $schemaAttributes = []; - foreach ($attributes as $key => $attribute) { + /* + * Loop though the collection attributes for creation of attributes in schema + * */ + foreach ($attributes as $attribute) { $attrId = $this->filter($attribute->getId()); - $attrType = $this->getSQLType($attribute->getAttribute('type'), $attribute->getAttribute('size', 0), $attribute->getAttribute('signed', true)); + $size = $attribute->getAttribute('size', 0); + $type = $attribute->getAttribute('type'); + $signed = $attribute->getAttribute('signed', false); + $attrType = $this->getSQLType($type, $size, $signed); if ($attribute->getAttribute('array')) { $attrType = 'LONGTEXT'; } - $attributes[$key] = "`{$attrId}` {$attrType}, "; + $schemaAttributes[] = "`{$attrId}` {$attrType}, "; } + /* + * Loop though the collection indexes for creation of indexes in schema + * */ + $schemaIndexes = []; foreach ($indexes as $key => $index) { + + $index = $this->fixIndex( + $index, + Database::filterIndexAttributes( + $index->getAttribute('attributes'), + $attributes + ) + ); + $indexId = $this->filter($index->getId()); $indexType = $index->getAttribute('type'); - - $indexAttributes = $index->getAttribute('attributes'); - foreach ($indexAttributes as $nested => $attribute) { - $indexLength = $index->getAttribute('lengths')[$key] ?? ''; - $indexLength = (empty($indexLength)) ? '' : '(' . (int)$indexLength . ')'; - $indexOrder = $index->getAttribute('orders')[$key] ?? ''; + $indexAttributes = []; + foreach ($index->getAttribute('attributes') as $attribute) { $indexAttribute = $this->filter($attribute); + $size = $index['lengths'][$key] ?? 0; + $length = $size === 0 ? '' : '(' . $size . ')'; + $indexOrder = $index->getAttribute('orders')[$key] ?? ''; if ($indexType === Database::INDEX_FULLTEXT) { $indexOrder = ''; } - $indexAttributes[$nested] = "`{$indexAttribute}`{$indexLength} {$indexOrder}"; + $indexAttributes[] = "`{$indexAttribute}`{$length} {$indexOrder}"; } - $indexes[$key] = "{$indexType} `{$indexId}` (" . \implode(", ", $indexAttributes) . " ),"; + $schemaIndexes[] = "{$indexType} `{$indexId}` (" . \implode(", ", $indexAttributes) . " ),"; } try { @@ -189,9 +208,9 @@ public function createCollection(string $name, array $attributes = [], array $in `_createdAt` datetime(3) DEFAULT NULL, `_updatedAt` datetime(3) DEFAULT NULL, `_permissions` MEDIUMTEXT DEFAULT NULL, - " . \implode(' ', $attributes) . " + " . \implode(' ', $schemaAttributes) . " PRIMARY KEY (`_id`), - " . \implode(' ', $indexes) . " + " . \implode(' ', $schemaIndexes) . " UNIQUE KEY `_uid` (`_uid`), KEY `_created_at` (`_createdAt`), KEY `_updated_at` (`_updatedAt`) @@ -363,36 +382,46 @@ public function deleteAttribute(string $collection, string $id, bool $array = fa * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes Document[] * @param array $lengths * @param array $orders * @return bool * @throws Exception - * @throws PDOException */ public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool { $name = $this->filter($collection); $id = $this->filter($id); - $attributes = \array_map(fn ($attribute) => match ($attribute) { - '$id' => ID::custom('_uid'), - '$createdAt' => '_createdAt', - '$updatedAt' => '_updatedAt', - default => $attribute - }, $attributes); + $index = new Document([ + '$id' => ID::custom($id), + 'key' => $id, + 'type' => $type, + 'attributes' => $attributes, + 'lengths' => $lengths, + 'orders' => $orders, + ]); + + $index = $this->fixIndex($index, $attributes); foreach ($attributes as $key => $attribute) { - $length = $lengths[$key] ?? ''; + $length = $index['lengths'][$key] ?? ''; $length = (empty($length)) ? '' : '(' . (int)$length . ')'; - $order = $orders[$key] ?? ''; - $attribute = $this->filter($attribute); + $order = $index['orders'][$key] ?? ''; + + $attributeName = $attribute->getId(); + // todo : make a function for this... + if($attributeName === '$id')$attributeName = '_uid'; + if($attributeName === '$createdAt')$attributeName = '_createdAt'; + if($attributeName === '$updatedAt')$attributeName = '_updatedAt'; + + $attributeName = $this->filter($attributeName); if (Database::INDEX_FULLTEXT === $type) { $order = ''; } - $attributes[$key] = "`{$attribute}`{$length} {$order}"; + $attributes[$key] = "`{$attributeName}`{$length} {$order}"; } return $this->getPDO() @@ -1138,6 +1167,16 @@ public function getLimitForIndexes(): int return 64; } + /** + * Get Maximum Length for index + * + * @return int + */ + public function getLimitForIndexLength(): int + { + return 0; + } + /** * Is schemas supported? * @@ -1147,7 +1186,7 @@ public function getSupportForSchemas(): bool { return true; } - + /** * Is index supported? * @@ -1213,7 +1252,7 @@ public static function getCountOfDefaultAttributes(): int { return 4; } - + /** * Returns number of indexes used by default. * @@ -1323,7 +1362,7 @@ public function getAttributeWidth(Document $collection): int /** * Get list of keywords that cannot be used * Refference: https://mariadb.com/kb/en/reserved-words/ - * + * * @return string[] */ public function getKeywords(): array @@ -1790,7 +1829,6 @@ protected function getSQLIndex(string $collection, string $id, string $type, ar default: throw new Exception('Unknown Index Type:' . $type); - break; } return "CREATE {$type} `{$id}` ON {$this->getSQLTable($collection)} ( " . implode(', ', $attributes) . " )"; @@ -1799,10 +1837,10 @@ protected function getSQLIndex(string $collection, string $id, string $type, ar /** * Get SQL condition for permissions * - * @param string $collection - * @param array $roles - * @return string - * @throws Exception + * @param string $collection + * @param array $roles + * @return string + * @throws Exception */ protected function getSQLPermissionsCondition(string $collection, array $roles): string { @@ -1818,7 +1856,7 @@ protected function getSQLPermissionsCondition(string $collection, array $roles): /** * Get SQL schema * - * @return string + * @return string */ protected function getSQLSchema(): string { @@ -1832,8 +1870,8 @@ protected function getSQLSchema(): string /** * Get SQL table * - * @param string $name - * @return string + * @param string $name + * @return string */ protected function getSQLTable(string $name): string { @@ -1845,32 +1883,21 @@ protected function getSQLTable(string $name): string * * @param mixed $value * @return int - * @throws Exception + * @throws Exception */ protected function getPDOType(mixed $value): int { - switch (gettype($value)) { - case 'double': - case 'string': - return PDO::PARAM_STR; - - case 'integer': - case 'boolean': - return PDO::PARAM_INT; - - //case 'float': // (for historical reasons "double" is returned in case of a float, and not simply "float") - - case 'NULL': - return PDO::PARAM_NULL; - - default: - throw new Exception('Unknown PDO Type for ' . gettype($value)); - } + return match (gettype($value)) { + 'double', 'string' => PDO::PARAM_STR, + 'integer', 'boolean' => PDO::PARAM_INT, + 'NULL' => PDO::PARAM_NULL, + default => throw new Exception('Unknown PDO Type for ' . gettype($value)), + }; } /** * Returns the current PDO object - * @return PDO + * @return PDO */ protected function getPDO() { @@ -1891,4 +1918,19 @@ public static function getPDOAttributes(): array PDO::ATTR_STRINGIFY_FETCHES => true // Returns all fetched data as Strings ]; } + + /** + * This function fixes indexes which has exceeded max default limits + * with comparing the length of the string length of the collection attribute + * + * @param Document $index + * @param Document[] $attributes + * @return Document + * @throws Exception + */ + public function fixIndex(Document $index, array $attributes): Document { + return $index; + } + + } diff --git a/src/Database/Adapter/Mongo/MongoDBAdapter.php b/src/Database/Adapter/Mongo/MongoDBAdapter.php index 9e8fde2c2..5f07be67b 100644 --- a/src/Database/Adapter/Mongo/MongoDBAdapter.php +++ b/src/Database/Adapter/Mongo/MongoDBAdapter.php @@ -287,7 +287,7 @@ public function renameAttribute(string $collection, string $id, string $name): b * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes Document[] * @param array $lengths * @param array $orders * diff --git a/src/Database/Adapter/MySQL.php b/src/Database/Adapter/MySQL.php index 693f41443..3311b6d1f 100644 --- a/src/Database/Adapter/MySQL.php +++ b/src/Database/Adapter/MySQL.php @@ -4,18 +4,20 @@ use Exception; use Utopia\Database\Database; +use Utopia\Database\Document; class MySQL extends MariaDB { /** * Get SQL Index - * + * * @param string $collection * @param string $id * @param string $type * @param array $attributes - * + * * @return string + * @throws Exception */ protected function getSQLIndex(string $collection, string $id, string $type, array $attributes): string { @@ -48,4 +50,31 @@ protected function getSQLIndex(string $collection, string $id, string $type, arr return 'CREATE '.$type.' `'.$id.'` ON `'.$this->getDefaultDatabase().'`.`'.$this->getNamespace().'_'.$collection.'` ( '.implode(', ', $attributes).' );'; } + /** + * This function fixes indexes which has exceeded max default limits + * with comparing the length of the string length of the collection attribute + * + * @param Document $index + * @param Document[] $attributes + * @return Document + * @throws Exception + */ + public function fixIndex(Document $index, array $attributes): Document { + + $max = 768; // 3072 divided by utf8mb4 + + foreach ($attributes as $key => $attribute){ + $size = $index['lengths'][$key] ?? 0; + + if($attribute['type'] === Database::VAR_STRING){ + if($attribute['size'] > $max){ + $index['lengths'][$key] = $size === 0 || $size > $max ? $max : $size; + } + } + + } + + return $index; + } + } diff --git a/src/Database/Adapter/SQLite.php b/src/Database/Adapter/SQLite.php index 44cc68c68..67d194aff 100644 --- a/src/Database/Adapter/SQLite.php +++ b/src/Database/Adapter/SQLite.php @@ -7,7 +7,6 @@ use PDOException; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\ID; use Utopia\Database\Exception\Duplicate; /** @@ -24,8 +23,45 @@ * 9. MODIFY COLUMN is not supported * 10. Can't rename an index directly */ -class SQLite extends MySQL +class SQLite extends MariaDB { + + /** + * List of permission attributes + * @var array + */ + + + protected static array $permissionAttributes = [ + [ + '$id' => '_document', + 'type' => Database::VAR_STRING, + 'size' => 12, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => '_type', + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => '_permission', + 'type' => Database::VAR_STRING, + 'size' => Database::LENGTH_KEY, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ] + ]; + /** * Check if Database exists * Optionally check if collection exists in Database @@ -99,8 +135,9 @@ public function createCollection(string $name, array $attributes = [], array $in $id = $this->filter($name); $this->getPDO()->beginTransaction(); + $schemaAttributes = []; - foreach ($attributes as $key => $attribute) { + foreach ($attributes as $attribute) { $attrId = $this->filter($attribute->getId()); $attrType = $this->getSQLType($attribute->getAttribute('type'), $attribute->getAttribute('size', 0), $attribute->getAttribute('signed', true)); @@ -108,7 +145,7 @@ public function createCollection(string $name, array $attributes = [], array $in $attrType = 'LONGTEXT'; } - $attributes[$key] = "`{$attrId}` {$attrType}, "; + $schemaAttributes[] = "`{$attrId}` {$attrType}, "; } $this->getPDO() @@ -117,19 +154,24 @@ public function createCollection(string $name, array $attributes = [], array $in `_uid` CHAR(255) NOT NULL, `_createdAt` datetime(3) DEFAULT NULL, `_updatedAt` datetime(3) DEFAULT NULL, - `_permissions` MEDIUMTEXT DEFAULT NULL".((!empty($attributes)) ? ',' : '')." - " . substr(\implode(' ', $attributes), 0, -2) . " + `_permissions` MEDIUMTEXT DEFAULT NULL".((!empty($schemaAttributes)) ? ',' : '')." + " . substr(\implode(' ', $schemaAttributes), 0, -2) . " )") ->execute(); - $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, ['_uid'], [], []); - $this->createIndex($id, '_created_at', Database::INDEX_KEY, ['_createdAt'], [], []); - $this->createIndex($id, '_updated_at', Database::INDEX_KEY, ['_updatedAt'], [], []); + $this->createIndex($id, '_index1', Database::INDEX_UNIQUE, Database::filterIndexAttributes(['$id'], $attributes), [], []); + $this->createIndex($id, '_created_at', Database::INDEX_KEY, Database::filterIndexAttributes(['$createdAt'], $attributes), [], []); + $this->createIndex($id, '_updated_at', Database::INDEX_KEY, Database::filterIndexAttributes(['$updatedAt'], $attributes), [], []); - foreach ($indexes as $key => $index) { + foreach ($indexes as $index) { $indexId = $this->filter($index->getId()); $indexType = $index->getAttribute('type'); - $indexAttributes = $index->getAttribute('attributes', []); + + $indexAttributes = Database::filterIndexAttributes( + $index->getAttribute('attributes'), + $attributes + ); + $indexLengths = $index->getAttribute('lengths', []); $indexOrders = $index->getAttribute('orders', []); @@ -148,9 +190,14 @@ public function createCollection(string $name, array $attributes = [], array $in } catch (\Throwable $th) { var_dump($th->getMessage()); } - - $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, ['_document', '_type', '_permission'], [], []); - $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, ['_permission'], [], []); + + $permissionAttributes = Database::changeArrayToDocuments(self::$permissionAttributes); + + $attributes = Database::filterIndexAttributes(['_document', '_type', '_permission'], $permissionAttributes); + $this->createIndex("{$id}_perms", '_index_1', Database::INDEX_UNIQUE, $attributes, [], []); + + $attributes = Database::filterIndexAttributes(['_permission'], $permissionAttributes); + $this->createIndex("{$id}_perms", '_index_2', Database::INDEX_KEY, $attributes, [], []); $this->getPDO()->commit(); @@ -214,8 +261,8 @@ public function updateAttribute(string $collection, string $id, string $type, in */ public function renameIndex(string $collection, string $old, string $new): bool { - $collection = $this->filter($collection); $collectionDocument = $this->getDocument(Database::METADATA, $collection); + $old = $this->filter($old); $new = $this->filter($new); $indexs = json_decode($collectionDocument['indexes'], true); @@ -228,12 +275,19 @@ public function renameIndex(string $collection, string $old, string $new): bool } } + $attributes = json_decode($collectionDocument['attributes'], true); + $attributes = Database::changeArrayToDocuments($attributes); + $attributes = Database::filterIndexAttributes( + $index['attributes'], + $attributes + ); + if ($index && $this->deleteIndex($collection, $old) && $this->createIndex( $collection, $new, $index['type'], - $index['attributes'], + $attributes, $index['lengths'], $index['orders'], )) { @@ -249,12 +303,11 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param string $collection * @param string $id * @param string $type - * @param array $attributes + * @param array $attributes Document[] * @param array $lengths * @param array $orders * @return bool * @throws Exception - * @throws PDOException */ public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths, array $orders): bool { @@ -639,23 +692,26 @@ protected function getSQLIndex(string $collection, string $id, string $type, ar default: throw new Exception('Unknown Index Type:' . $type); - break; } - $attributes = \array_map(fn ($attribute) => match ($attribute) { - '$id' => ID::custom('_uid'), - '$createdAt' => '_createdAt', - '$updatedAt' => '_updatedAt', - default => $attribute - }, $attributes); - foreach ($attributes as $key => $attribute) { - $length = $lengths[$key] ?? ''; + $length = $index['lengths'][$key] ?? ''; $length = (empty($length)) ? '' : '(' . (int)$length . ')'; - $order = $orders[$key] ?? ''; - $attribute = $this->filter($attribute); + $order = $index['orders'][$key] ?? ''; + + $attributeName = $attribute->getId(); + // todo : make a function for this... + if($attributeName === '$id')$attributeName = '_uid'; + if($attributeName === '$createdAt')$attributeName = '_createdAt'; + if($attributeName === '$updatedAt')$attributeName = '_updatedAt'; + + $attributeName = $this->filter($attributeName); + + if (Database::INDEX_FULLTEXT === $type) { + $order = ''; + } - $attributes[$key] = "`{$attribute}`{$postfix} {$order}"; + $attributes[$key] = "`{$attributeName}`{$postfix} {$order}"; } return "CREATE {$type} `{$this->getNamespace()}_{$collection}_{$id}` ON `{$this->getNamespace()}_{$collection}` ( " . implode(', ', $attributes) . ")"; diff --git a/src/Database/Database.php b/src/Database/Database.php index e9e93c3e8..1e0f7674a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5,10 +5,10 @@ use Exception; use Throwable; use Utopia\Database\Exception\Duplicate; +use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Structure; use Utopia\Database\Exception\Authorization as AuthorizationException; -use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; use Utopia\Database\Exception\Structure as StructureException; use Utopia\Cache\Cache; @@ -118,7 +118,7 @@ class Database * List of Internal Ids * @var array */ - protected array $attributes = [ + protected static array $attributes = [ [ '$id' => '$id', 'type' => self::VAR_STRING, @@ -431,6 +431,7 @@ public function ping(): bool * @param string $name * * @return bool + * @throws Exception|Throwable */ public function create(string $name): bool { @@ -439,7 +440,6 @@ public function create(string $name): bool /** * Create array of attribute documents - * @var Document[] $attributes */ $attributes = array_map(function ($attribute) { return new Document([ @@ -508,6 +508,8 @@ public function delete(string $name): bool * @param Document[] $indexes (optional) * * @return Document + * @throws DuplicateException + * @throws Exception|Throwable */ public function createCollection(string $id, array $attributes = [], array $indexes = []): Document { @@ -566,12 +568,15 @@ public function createCollection(string $id, array $attributes = [], array $inde * Get Collection * * @param string $id - * * @return Document - * @throws Exception + * @throws Throwable */ public function getCollection(string $id): Document { + if(empty($id)){ + throw new Exception("getCollection requires a parameter"); + } + $collection = $this->silent(fn() => $this->getDocument(self::METADATA, $id)); $this->trigger(self::EVENT_COLLECTION_READ, $collection); return $collection; @@ -621,23 +626,29 @@ public function deleteCollection(string $id): bool /** * Create Attribute * - * @param string $collection + * @param string $collectionName * @param string $id * @param string $type * @param int $size utf8mb4 chars length * @param bool $required - * @param array|bool|callable|int|float|object|resource|string|null $default + * @param null $default * @param bool $signed * @param bool $array - * @param string $format optional validation format of attribute - * @param string $formatOptions assoc array with custom options that can be passed for the format validation + * @param string|null $format optional validation format of attribute + * @param array $formatOptions assoc array with custom options that can be passed for the format validation * @param array $filters * * @return bool + * @throws DuplicateException + * @throws LimitException + * @throws Throwable */ - public function createAttribute(string $collection, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, string $format = null, array $formatOptions = [], array $filters = []): bool + public function createAttribute(string $collectionName, string $id, string $type, int $size, bool $required, $default = null, bool $signed = true, bool $array = false, string $format = null, array $formatOptions = [], array $filters = []): bool { - $collection = $this->silent(fn() => $this->getCollection($collection)); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' . $collectionName . ' Not found'); + } // attribute IDs are case insensitive $attributes = $collection->getAttribute('attributes', []); @@ -806,10 +817,13 @@ protected function validateDefaultTypes(string $type, mixed $default): void * * @return Document */ - private function updateAttributeMeta(string $collection, string $id, callable $updateCallback): void + private function updateAttributeMeta(string $collectionName, string $id, callable $updateCallback): void { // Load - $collection = $this->silent(fn() => $this->getCollection($collection)); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' . $collectionName . ' Not found'); + } $attributes = $collection->getAttribute('attributes', []); @@ -1050,6 +1064,7 @@ public function deleteAttribute(string $collection, string $id): bool } } + $attributes = array_values($attributes); $collection->setAttribute('attributes', $attributes); if ($collection->getId() !== self::METADATA) { @@ -1064,15 +1079,19 @@ public function deleteAttribute(string $collection, string $id): bool /** * Rename Attribute * - * @param string $collection + * @param string $collectionName * @param string $old Current attribute ID - * @param string $name New attribute ID - * + * @param string $new * @return bool + * @throws DuplicateException + * @throws Throwable */ - public function renameAttribute(string $collection, string $old, string $new): bool + public function renameAttribute(string $collectionName, string $old, string $new): bool { - $collection = $this->silent(fn() => $this->getCollection($collection)); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' . $collectionName . ' Not found'); + } $attributes = $collection->getAttribute('attributes', []); $indexes = $collection->getAttribute('indexes', []); @@ -1125,10 +1144,14 @@ public function renameAttribute(string $collection, string $old, string $new): b * @param string $new * * @return bool + * @throws Exception|Throwable */ - public function renameIndex(string $collection, string $old, string $new): bool + public function renameIndex(string $collectionName, string $old, string $new): bool { - $collection = $this->silent(fn() => $this->getCollection($collection)); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' . $collectionName . ' Not found'); + } $indexes = $collection->getAttribute('indexes', []); @@ -1168,7 +1191,7 @@ public function renameIndex(string $collection, string $old, string $new): bool /** * Create Index * - * @param string $collection + * @param string $collectionName * @param string $id * @param string $type * @param array $attributes @@ -1176,15 +1199,21 @@ public function renameIndex(string $collection, string $old, string $new): bool * @param array $orders * * @return bool + * @throws DuplicateException + * @throws LimitException + * @throws Throwable */ - public function createIndex(string $collection, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool + public function createIndex(string $collectionName, string $id, string $type, array $attributes, array $lengths = [], array $orders = []): bool { if (empty($attributes)) { throw new Exception('Missing attributes'); } - $collection = $this->silent(fn() => $this->getCollection($collection)); - + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' . $collectionName . ' Not found'); + } + // index IDs are case insensitive $indexes = $collection->getAttribute('indexes', []); /** @var Document[] $indexes */ @@ -1219,28 +1248,66 @@ public function createIndex(string $collection, string $id, string $type, array default: throw new Exception('Unknown index type: ' . $type); - break; } - $index = $this->adapter->createIndex($collection->getId(), $id, $type, $attributes, $lengths, $orders); - - $collection->setAttribute('indexes', new Document([ + $index = new Document([ '$id' => ID::custom($id), 'key' => $id, 'type' => $type, 'attributes' => $attributes, 'lengths' => $lengths, 'orders' => $orders, - ]), Document::SET_TYPE_APPEND); + ]); + + $attributes = $this->filterIndexAttributes( + $attributes, + $collection->getAttribute('attributes', []) + ); + + $result = $this->adapter->createIndex( + $collection->getId(), + $id, + $type, + $attributes, + $index->getAttribute('lengths'), + $orders + ); + + $collection->setAttribute('indexes', $index, Document::SET_TYPE_APPEND); if ($collection->getId() !== self::METADATA) { $this->silent(fn() => $this->updateDocument(self::METADATA, $collection->getId(), $collection)); } - $this->trigger(self::EVENT_INDEX_CREATE, $index); - return $index; + $this->trigger(self::EVENT_INDEX_CREATE, $result); + return $result; + } + + + /** + * Filter collection attributes by index attributes names + * + * @param array $indexAttributes + * @param array $attributes Documents[] + * @return array + * @throws Exception + */ + public static function filterIndexAttributes(array $indexAttributes, array $attributes): array + { + $internals = array_map( + fn ($attribute) => new Document($attribute), + self::$attributes + ); + + $attributes = array_filter( + array_merge($internals, $attributes), + fn ($attribute) => in_array($attribute->getId(), $indexAttributes) + ); + + return array_values($attributes); } + /** * Delete Index * @@ -1248,10 +1315,14 @@ public function createIndex(string $collection, string $id, string $type, array * @param string $id * * @return bool + * @throws Exception */ - public function deleteIndex(string $collection, string $id): bool + public function deleteIndex(string $collectionName, string $id): bool { - $collection = $this->silent(fn() => $this->getCollection($collection)); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collectionName . ' Not found'); + } $indexes = $collection->getAttribute('indexes', []); @@ -1263,6 +1334,7 @@ public function deleteIndex(string $collection, string $id): bool } } + $indexes = array_values($indexes); $collection->setAttribute('indexes', $indexes); if ($collection->getId() !== self::METADATA) { @@ -1281,21 +1353,19 @@ public function deleteIndex(string $collection, string $id): bool * @param string $id * * @return Document + * @throws Exception|Throwable */ - public function getDocument(string $collection, string $id): Document + public function getDocument(string $collectionName, string $id): Document { - if ($collection === self::METADATA && $id === self::METADATA) { + if ($collectionName === self::METADATA && $id === self::METADATA) { return new Document($this->collection); } - if (empty($collection)) { - throw new Exception('test exception: ' . $collection . ':' . $id); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collectionName . ' Not found'); } - $collection = $this->silent(fn() => $this->getCollection($collection)); - $document = null; - $cache = null; - $validator = new Authorization(self::PERMISSION_READ); // TODO@kodumbeats Check if returned cache id matches request @@ -1342,11 +1412,14 @@ public function getDocument(string $collection, string $id): Document * * @throws AuthorizationException * @throws StructureException - * @throws Exception + * @throws Exception|Throwable */ public function createDocument(string $collection, Document $document): Document { $collection = $this->silent(fn() => $this->getCollection($collection)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collection->getId() . ' Not found'); + } $time = DateTime::now(); @@ -1393,6 +1466,9 @@ public function updateDocument(string $collection, string $id, Document $documen $old = Authorization::skip(fn() => $this->silent(fn() => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this $collection = $this->silent(fn() => $this->getCollection($collection)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collection->getId() . ' Not found'); + } $validator = new Authorization(self::PERMISSION_UPDATE); @@ -1427,6 +1503,7 @@ public function updateDocument(string $collection, string $id, Document $documen * @return bool * * @throws AuthorizationException + * @throws Exception */ public function deleteDocument(string $collection, string $id): bool { @@ -1434,6 +1511,9 @@ public function deleteDocument(string $collection, string $id): bool $document = Authorization::skip(fn() => $this->silent(fn() => $this->getDocument($collection, $id))); // Skip ensures user does not need read permission for this $collection = $this->silent(fn() => $this->getCollection($collection)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collection->getId() . ' Not found'); + } if ($collection->getId() !== self::METADATA && !$validator->isValid($document->getDelete())) { @@ -1484,6 +1564,9 @@ public function deleteCachedDocument(string $collection, string $id): bool public function find(string $collection, array $queries = []): array { $collection = $this->silent(fn() => $this->getCollection($collection)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collection->getId() . ' Not found'); + } $grouped = Query::groupByType($queries); /** @var Query[] */ $filters = $grouped['filters']; @@ -1552,9 +1635,8 @@ public function findOne(string $collection, array $queries = []): bool|Document public function count(string $collection, array $queries = [], int $max = 0): int { $collection = $this->silent(fn() => $this->getCollection($collection)); - - if ($collection->isEmpty()) { - throw new Exception("Collection not found"); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collection->getId() . ' Not found'); } $queries = Query::groupByType($queries)['filters']; @@ -1578,12 +1660,11 @@ public function count(string $collection, array $queries = [], int $max = 0): in * @return int|float * @throws Exception */ - public function sum(string $collection, string $attribute, array $queries = [], int $max = 0) + public function sum(string $collectionName, string $attribute, array $queries = [], int $max = 0) { - $collection = $this->silent(fn() => $this->getCollection($collection)); - - if ($collection->isEmpty()) { - throw new Exception("Collection not found"); + $collection = $this->silent(fn() => $this->getCollection($collectionName)); + if($collection->isEmpty()){ + throw new Exception('Collection ' .$collectionName . ' Not found'); } $queries = self::convertQueries($collection, $queries); @@ -1615,11 +1696,7 @@ static public function addFilter(string $name, callable $encode, callable $decod */ public function getInternalAttributes(): array { - $attributes = []; - foreach ($this->attributes as $internal){ - $attributes[] = new Document($internal); - } - return $attributes; + return self::changeArrayToDocuments(self::$attributes); } /** @@ -1900,4 +1977,19 @@ public static function convertQueries(Document $collection, array $queries): arr } return $queries; } + + + /** + * @param array $array + * @return Document[] + * @throws Exception + */ + public static function changeArrayToDocuments(array $array): array + { + return array_map( + fn ($attribute) => new Document($attribute), + $array + ); + } + } diff --git a/tests/Database/Adapter/database.sql b/tests/Database/Adapter/database.sql index 723f8b62a..ad8e44497 100644 Binary files a/tests/Database/Adapter/database.sql and b/tests/Database/Adapter/database.sql differ diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 1500ce824..3858b26d6 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -283,6 +283,7 @@ public function testCreateDeleteIndex() $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'integer', Database::VAR_INTEGER, 0, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'float', Database::VAR_FLOAT, 0, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'boolean', Database::VAR_BOOLEAN, 0, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('indexes', 'long_varchar', Database::VAR_STRING, 2048, true)); // Indexes $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index1', Database::INDEX_KEY, ['string', 'integer'], [128], [Database::ORDER_ASC])); @@ -291,9 +292,11 @@ public function testCreateDeleteIndex() $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index4', Database::INDEX_UNIQUE, ['string'], [128], [Database::ORDER_ASC])); $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'index5', Database::INDEX_UNIQUE, ['$id', 'string'], [128], [Database::ORDER_ASC])); $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'order', Database::INDEX_UNIQUE, ['order'], [128], [Database::ORDER_ASC])); + $this->assertEquals(true, static::getDatabase()->createIndex('indexes', 'long_varchar', Database::INDEX_KEY, ['long_varchar'], [], [Database::ORDER_ASC])); + $collection = static::getDatabase()->getCollection('indexes'); - $this->assertCount(6, $collection->getAttribute('indexes')); + $this->assertCount(7, $collection->getAttribute('indexes')); // Delete Indexes $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index1')); @@ -302,6 +305,7 @@ public function testCreateDeleteIndex() $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index4')); $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'index5')); $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'order')); + $this->assertEquals(true, static::getDatabase()->deleteIndex('indexes', 'long_varchar')); $collection = static::getDatabase()->getCollection('indexes'); $this->assertCount(0, $collection->getAttribute('indexes')); @@ -315,7 +319,7 @@ public function testCreateCollectionWithSchema() new Document([ '$id' => ID::custom('attribute1'), 'type' => Database::VAR_STRING, - 'size' => 256, + 'size' => 2500, // more than 768 index max 'required' => false, 'signed' => true, 'array' => false, @@ -346,7 +350,7 @@ public function testCreateCollectionWithSchema() '$id' => ID::custom('index1'), 'type' => Database::INDEX_KEY, 'attributes' => ['attribute1'], - 'lengths' => [256], + 'lengths' => [], 'orders' => ['ASC'], ]), new Document([