Compare commits

...
Author SHA1 Message Date
Jake Barnby 22b4cc9e8b Merge remote-tracking branch 'origin/1.8.x' into feat-multi-create 2025-12-19 00:09:43 +13:00
Jake Barnby 59897433a4 Merge branch 'chore-specs' into feat-multi-create
# Conflicts:
#	composer.lock
2025-12-19 00:09:34 +13:00
Jake Barnby f99d929628 Map attributes to columns 2025-12-18 23:31:24 +13:00
Jake Barnby 3333031433 Map float 2025-12-18 23:31:10 +13:00
Jake Barnby 5074988700 Add more test coverage 2025-12-18 23:30:59 +13:00
Jake Barnby 7d7e861125 Handle terminology 2025-12-08 20:39:25 +13:00
6 changed files with 1933 additions and 54 deletions
@@ -124,9 +124,18 @@ class Create extends Action
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
$databaseKey = 'database_' . $database->getSequence();
// Map 'float' to 'double' (Database::VAR_FLOAT) before validation
$attributes = array_map(function ($attr) {
if (isset($attr['type']) && $attr['type'] === 'float') {
$attr['type'] = Database::VAR_FLOAT;
}
return $attr;
}, $attributes);
$attributesValidator = new AttributesValidator(
APP_LIMIT_ARRAY_PARAMS_SIZE,
$dbForProject->getAdapter()->getSupportForSpatialAttributes()
$dbForProject->getAdapter()->getSupportForSpatialAttributes(),
$this->isCollectionsAPI() ? AttributesValidator::TYPE_LEGACY : AttributesValidator::TYPE_TABLESDB
);
if (!$attributesValidator->isValid($attributes)) {
@@ -134,10 +143,11 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $attributesValidator->getDescription());
}
$structureTerm = $this->isCollectionsAPI() ? 'attributes' : 'columns';
foreach ($attributes as $attribute) {
if (($attribute['type'] ?? '') === Database::VAR_RELATIONSHIP) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Relationship attributes cannot be created inline. Use the create relationship endpoint instead.');
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, "Relationship {$structureTerm} cannot be created inline. Use the create relationship endpoint instead.");
}
}
@@ -155,7 +165,10 @@ class Create extends Action
}
// Validate indexes
$indexesValidator = new IndexesValidator($dbForProject->getLimitForIndexes());
$indexesValidator = new IndexesValidator(
$dbForProject->getLimitForIndexes(),
$this->isCollectionsAPI() ? IndexesValidator::TYPE_LEGACY : IndexesValidator::TYPE_TABLESDB
);
if (!$indexesValidator->isValid($indexes)) {
$dbForProject->deleteDocument($databaseKey, $collection->getId());
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $indexesValidator->getDescription());
@@ -186,7 +199,9 @@ class Create extends Action
$dbForProject->getAdapter()->getSupportForVectors(),
$dbForProject->getAdapter()->getSupportForAttributes(),
$dbForProject->getAdapter()->getSupportForMultipleFulltextIndexes(),
$dbForProject->getAdapter()->getSupportForIdenticalIndexes()
$dbForProject->getAdapter()->getSupportForIdenticalIndexes(),
$dbForProject->getAdapter()->getSupportForObject(),
);
foreach ($collectionIndexes as $indexDoc) {
@@ -237,6 +252,12 @@ class Create extends Action
$dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $collection->getId());
$dbForProject->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
// Set attributes and indexes on the collection document for the response
// Use the full database documents for attributes (they have status and other fields needed for response models)
// Use the simpler collection documents for indexes (they have the basic fields without internal tracking)
$collection->setAttribute('attributes', $attributeDocuments);
$collection->setAttribute('indexes', $collectionIndexes);
$queueForEvents
->setContext('database', $database)
->setParam('databaseId', $databaseId)
@@ -14,9 +14,18 @@ use Utopia\Validator\URL;
class Attributes extends Validator
{
public const TYPE_LEGACY = 'legacy';
public const TYPE_TABLESDB = 'tablesdb';
protected int $maxAttributes;
protected string $message = 'Invalid attributes';
/**
* @var string The term to use in error messages (attributes/columns)
*/
protected string $termPlural;
protected string $termSingular;
/**
* @var array<string> Supported attribute types
*/
@@ -45,12 +54,23 @@ class Attributes extends Validator
/**
* @param int $maxAttributes Maximum number of attributes allowed
* @param bool $supportForSpatialAttributes Whether DB supports spatial attributes
* @param string $type The API type context ('legacy' for attributes, 'tablesdb' for columns)
*/
public function __construct(
int $maxAttributes = APP_LIMIT_ARRAY_PARAMS_SIZE,
protected bool $supportForSpatialAttributes = true,
string $type = self::TYPE_LEGACY,
) {
$this->maxAttributes = $maxAttributes;
// Set terminology based on API type
if ($type === self::TYPE_TABLESDB) {
$this->termPlural = 'Columns';
$this->termSingular = 'Column';
} else {
$this->termPlural = 'Attributes';
$this->termSingular = 'Attribute';
}
}
/**
@@ -73,13 +93,16 @@ class Attributes extends Validator
*/
public function isValid($value): bool
{
$termLower = \strtolower($this->termSingular);
$termPluralLower = \strtolower($this->termPlural);
if (!\is_array($value)) {
$this->message = 'Attributes must be an array';
$this->message = "{$this->termPlural} must be an array";
return false;
}
if (\count($value) > $this->maxAttributes) {
$this->message = 'Maximum of ' . $this->maxAttributes . ' attributes allowed';
$this->message = "Maximum of {$this->maxAttributes} {$termPluralLower} allowed";
return false;
}
@@ -88,30 +111,30 @@ class Attributes extends Validator
foreach ($value as $index => $attribute) {
if (!\is_array($attribute)) {
$this->message = "Attribute at index $index must be an object";
$this->message = "{$this->termSingular} at index $index must be an object";
return false;
}
// Validate required fields
if (!isset($attribute['key'])) {
$this->message = "Attribute at index $index is missing required field 'key'";
$this->message = "{$this->termSingular} at index $index is missing required field 'key'";
return false;
}
if (!isset($attribute['type'])) {
$this->message = "Attribute at index $index is missing required field 'type'";
$this->message = "{$this->termSingular} at index $index is missing required field 'type'";
return false;
}
// Validate key
if (!$keyValidator->isValid($attribute['key'])) {
$this->message = "Invalid key for attribute at index $index: " . $keyValidator->getDescription();
$this->message = "Invalid key for {$termLower} at index $index: " . $keyValidator->getDescription();
return false;
}
// Check for duplicate keys
if (in_array($attribute['key'], $keys)) {
$this->message = "Duplicate attribute key: " . $attribute['key'];
$this->message = "Duplicate {$termLower} key: " . $attribute['key'];
return false;
}
$keys[] = $attribute['key'];
@@ -119,26 +142,26 @@ class Attributes extends Validator
// Check for reserved keys
$reservedKeys = ['$id', '$createdAt', '$updatedAt', '$permissions', '$collection'];
if (\in_array($attribute['key'], $reservedKeys)) {
$this->message = "Attribute key '" . $attribute['key'] . "' is reserved and cannot be used";
$this->message = "{$this->termSingular} key '" . $attribute['key'] . "' is reserved and cannot be used";
return false;
}
// Validate type
if (!\in_array($attribute['type'], $this->supportedTypes)) {
$this->message = "Invalid type for attribute '" . $attribute['key'] . "': " . $attribute['type'];
$this->message = "Invalid type for {$termLower} '" . $attribute['key'] . "': " . $attribute['type'];
return false;
}
// Validate spatial type support
if (\in_array($attribute['type'], Database::SPATIAL_TYPES) && !$this->supportForSpatialAttributes) {
$this->message = "Spatial attributes are not supported by the current database";
$this->message = "Spatial {$termPluralLower} are not supported by the current database";
return false;
}
// Validate size for string types
if ($attribute['type'] === Database::VAR_STRING) {
if (!isset($attribute['size']) || !is_int($attribute['size']) || $attribute['size'] < 1 || $attribute['size'] > APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH) {
$this->message = "Invalid or missing size for string attribute '" . $attribute['key'] . "'. Size must be between 1 and " . APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH;
$this->message = "Invalid or missing size for string {$termLower} '" . $attribute['key'] . "'. Size must be between 1 and " . APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH;
return false;
}
}
@@ -147,61 +170,61 @@ class Attributes extends Validator
if (isset($attribute['format']) && $attribute['format'] !== '') {
// Format is only allowed for string type
if ($attribute['type'] !== Database::VAR_STRING) {
$this->message = "Format is only allowed for string type for attribute '" . $attribute['key'] . "'";
$this->message = "Format is only allowed for string type for {$termLower} '" . $attribute['key'] . "'";
return false;
}
if (!in_array($attribute['format'], $this->supportedFormats)) {
$this->message = "Invalid format for attribute '" . $attribute['key'] . "': " . $attribute['format'];
$this->message = "Invalid format for {$termLower} '" . $attribute['key'] . "': " . $attribute['format'];
return false;
}
}
// Validate required field if provided
if (isset($attribute['required']) && !is_bool($attribute['required'])) {
$this->message = "Invalid 'required' value for attribute '" . $attribute['key'] . "': must be a boolean";
$this->message = "Invalid 'required' value for {$termLower} '" . $attribute['key'] . "': must be a boolean";
return false;
}
// Validate array field if provided
if (isset($attribute['array']) && !is_bool($attribute['array'])) {
$this->message = "Invalid 'array' value for attribute '" . $attribute['key'] . "': must be a boolean";
$this->message = "Invalid 'array' value for {$termLower} '" . $attribute['key'] . "': must be a boolean";
return false;
}
// Validate signed field if provided
if (isset($attribute['signed']) && !is_bool($attribute['signed'])) {
$this->message = "Invalid 'signed' value for attribute '" . $attribute['key'] . "': must be a boolean";
$this->message = "Invalid 'signed' value for {$termLower} '" . $attribute['key'] . "': must be a boolean";
return false;
}
// Validate signed only for integer/float types
if (isset($attribute['signed']) && !in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
$this->message = "Attribute '" . $attribute['key'] . "': 'signed' can only be used with integer or float types";
$this->message = "{$this->termSingular} '" . $attribute['key'] . "': 'signed' can only be used with integer or float types";
return false;
}
// Validate required and default conflict
if (isset($attribute['required']) && $attribute['required'] === true && isset($attribute['default']) && $attribute['default'] !== null) {
$this->message = "Attribute '" . $attribute['key'] . "' cannot have a default value when required is true";
$this->message = "{$this->termSingular} '" . $attribute['key'] . "' cannot have a default value when required is true";
return false;
}
// Validate array and default conflict
if (isset($attribute['array']) && $attribute['array'] === true && isset($attribute['default']) && $attribute['default'] !== null) {
$this->message = "Attribute '" . $attribute['key'] . "' cannot have a default value when array is true";
$this->message = "{$this->termSingular} '" . $attribute['key'] . "' cannot have a default value when array is true";
return false;
}
// Validate min/max range for integer/float
if (isset($attribute['min']) || isset($attribute['max'])) {
if (!in_array($attribute['type'], [Database::VAR_INTEGER, Database::VAR_FLOAT])) {
$this->message = "Attribute '" . $attribute['key'] . "': min/max can only be used with integer or float types";
$this->message = "{$this->termSingular} '" . $attribute['key'] . "': min/max can only be used with integer or float types";
return false;
}
// If both are set, validate ordering
if (isset($attribute['min']) && isset($attribute['max']) && $attribute['min'] > $attribute['max']) {
$this->message = "Attribute '" . $attribute['key'] . "': minimum value must be less than or equal to maximum value";
$this->message = "{$this->termSingular} '" . $attribute['key'] . "': minimum value must be less than or equal to maximum value";
return false;
}
}
@@ -211,7 +234,7 @@ class Attributes extends Validator
switch ($attribute['type']) {
case Database::VAR_STRING:
if (!is_string($attribute['default'])) {
$this->message = "Default value for string attribute '" . $attribute['key'] . "' must be a string";
$this->message = "Default value for string {$termLower} '" . $attribute['key'] . "' must be a string";
return false;
}
@@ -220,7 +243,7 @@ class Attributes extends Validator
if ($size > 0) {
$textValidator = new Text($size, 0);
if (!$textValidator->isValid($attribute['default'])) {
$this->message = "Default value for attribute '" . $attribute['key'] . "' exceeds maximum size of $size characters";
$this->message = "Default value for {$termLower} '" . $attribute['key'] . "' exceeds maximum size of $size characters";
return false;
}
}
@@ -230,19 +253,19 @@ class Attributes extends Validator
if ($format === APP_DATABASE_ATTRIBUTE_EMAIL) {
$emailValidator = new Email();
if (!$emailValidator->isValid($attribute['default'])) {
$this->message = "Default value for email attribute '" . $attribute['key'] . "' must be a valid email address";
$this->message = "Default value for email {$termLower} '" . $attribute['key'] . "' must be a valid email address";
return false;
}
} elseif ($format === APP_DATABASE_ATTRIBUTE_IP) {
$ipValidator = new IP();
if (!$ipValidator->isValid($attribute['default'])) {
$this->message = "Default value for IP attribute '" . $attribute['key'] . "' must be a valid IP address";
$this->message = "Default value for IP {$termLower} '" . $attribute['key'] . "' must be a valid IP address";
return false;
}
} elseif ($format === APP_DATABASE_ATTRIBUTE_URL) {
$urlValidator = new URL();
if (!$urlValidator->isValid($attribute['default'])) {
$this->message = "Default value for URL attribute '" . $attribute['key'] . "' must be a valid URL";
$this->message = "Default value for URL {$termLower} '" . $attribute['key'] . "' must be a valid URL";
return false;
}
}
@@ -250,7 +273,7 @@ class Attributes extends Validator
case Database::VAR_INTEGER:
if (!is_int($attribute['default'])) {
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be an integer";
$this->message = "Default value for integer {$termLower} '" . $attribute['key'] . "' must be an integer";
return false;
}
// Validate within range if min/max specified
@@ -259,7 +282,7 @@ class Attributes extends Validator
$max = $attribute['max'] ?? \PHP_INT_MAX;
$rangeValidator = new Range($min, $max, Database::VAR_INTEGER);
if (!$rangeValidator->isValid($attribute['default'])) {
$this->message = "Default value for integer attribute '" . $attribute['key'] . "' must be between $min and $max";
$this->message = "Default value for integer {$termLower} '" . $attribute['key'] . "' must be between $min and $max";
return false;
}
}
@@ -267,7 +290,7 @@ class Attributes extends Validator
case Database::VAR_FLOAT:
if (!is_float($attribute['default']) && !is_int($attribute['default'])) {
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be a number";
$this->message = "Default value for float {$termLower} '" . $attribute['key'] . "' must be a number";
return false;
}
// Validate within range if min/max specified
@@ -276,7 +299,7 @@ class Attributes extends Validator
$max = $attribute['max'] ?? \PHP_FLOAT_MAX;
$rangeValidator = new Range($min, $max, Database::VAR_FLOAT);
if (!$rangeValidator->isValid((float)$attribute['default'])) {
$this->message = "Default value for float attribute '" . $attribute['key'] . "' must be between $min and $max";
$this->message = "Default value for float {$termLower} '" . $attribute['key'] . "' must be between $min and $max";
return false;
}
}
@@ -284,20 +307,20 @@ class Attributes extends Validator
case Database::VAR_BOOLEAN:
if (!is_bool($attribute['default'])) {
$this->message = "Default value for boolean attribute '" . $attribute['key'] . "' must be a boolean";
$this->message = "Default value for boolean {$termLower} '" . $attribute['key'] . "' must be a boolean";
return false;
}
break;
case Database::VAR_DATETIME:
if (!is_string($attribute['default'])) {
$this->message = "Default value for datetime attribute '" . $attribute['key'] . "' must be a string in ISO 8601 format";
$this->message = "Default value for datetime {$termLower} '" . $attribute['key'] . "' must be a string in ISO 8601 format";
return false;
}
// Basic datetime format validation
$datetimeValidator = new DatetimeValidator();
if (!$datetimeValidator->isValid($attribute['default'])) {
$this->message = "Default value for datetime attribute '" . $attribute['key'] . "' must be in valid ISO 8601 format";
$this->message = "Default value for datetime {$termLower} '" . $attribute['key'] . "' must be in valid ISO 8601 format";
return false;
}
break;
@@ -307,18 +330,18 @@ class Attributes extends Validator
// Validate enum elements if format is enum
if (isset($attribute['format']) && $attribute['format'] === APP_DATABASE_ATTRIBUTE_ENUM) {
if (!isset($attribute['elements']) || !is_array($attribute['elements']) || empty($attribute['elements'])) {
$this->message = "Attribute '" . $attribute['key'] . "' with enum format must have 'elements' array";
$this->message = "{$this->termSingular} '" . $attribute['key'] . "' with enum format must have 'elements' array";
return false;
}
// Validate each enum element
foreach ($attribute['elements'] as $elementIndex => $element) {
if (!is_string($element) || empty($element)) {
$this->message = "Enum element at index $elementIndex for attribute '" . $attribute['key'] . "' must be a non-empty string";
$this->message = "Enum element at index $elementIndex for {$termLower} '" . $attribute['key'] . "' must be a non-empty string";
return false;
}
if (strlen($element) > Database::LENGTH_KEY) {
$this->message = "Enum element at index $elementIndex for attribute '" . $attribute['key'] . "' exceeds maximum length of " . Database::LENGTH_KEY . " characters";
$this->message = "Enum element at index $elementIndex for {$termLower} '" . $attribute['key'] . "' exceeds maximum length of " . Database::LENGTH_KEY . " characters";
return false;
}
}
@@ -326,7 +349,7 @@ class Attributes extends Validator
// Validate default exists in elements
if (isset($attribute['default']) && $attribute['default'] !== null) {
if (!in_array($attribute['default'], $attribute['elements'], true)) {
$this->message = "Default value for enum attribute '" . $attribute['key'] . "' must be one of the provided elements";
$this->message = "Default value for enum {$termLower} '" . $attribute['key'] . "' must be one of the provided elements";
return false;
}
}
@@ -8,8 +8,16 @@ use Utopia\Validator;
class Indexes extends Validator
{
public const TYPE_LEGACY = 'legacy';
public const TYPE_TABLESDB = 'tablesdb';
protected string $message = 'Invalid indexes';
/**
* @var string The term to use in error messages for attributes/columns
*/
protected string $attributeTerm;
/**
* @var array<string> Supported index types
*/
@@ -30,10 +38,14 @@ class Indexes extends Validator
/**
* @param int $maxIndexes Maximum number of indexes allowed
* @param string $type The API type context ('legacy' for attributes, 'tablesdb' for columns)
*/
public function __construct(
protected int $maxIndexes = APP_LIMIT_ARRAY_PARAMS_SIZE,
string $type = self::TYPE_LEGACY,
) {
// Set terminology based on API type
$this->attributeTerm = ($type === self::TYPE_TABLESDB) ? 'column' : 'attribute';
}
/**
@@ -84,11 +96,21 @@ class Indexes extends Validator
return false;
}
if (!isset($index['attributes']) || !is_array($index['attributes'])) {
$this->message = "Index at position $i is missing required field 'attributes' (must be an array)";
$attributesFieldName = ($this->attributeTerm === 'column') ? 'columns' : 'attributes';
if (!isset($index['attributes']) && !isset($index['columns'])) {
$this->message = "Index at position $i is missing required field '{$attributesFieldName}' (must be an array)";
return false;
}
// Support both 'attributes' and 'columns' field names for TablesDB compatibility
$indexAttributes = $index['attributes'] ?? $index['columns'] ?? null;
if (!is_array($indexAttributes)) {
$this->message = "Index at position $i is missing required field '{$attributesFieldName}' (must be an array)";
return false;
}
// Normalize to 'attributes' for internal processing
$index['attributes'] = $indexAttributes;
// Validate key format
if (!$keyValidator->isValid($index['key'])) {
$this->message = "Invalid key for index at position $i: " . $keyValidator->getDescription();
@@ -110,12 +132,12 @@ class Indexes extends Validator
// Validate attributes array
if (empty($index['attributes'])) {
$this->message = "Index '" . $index['key'] . "' must have at least one attribute";
$this->message = "Index '" . $index['key'] . "' must have at least one {$this->attributeTerm}";
return false;
}
if (count($index['attributes']) > APP_LIMIT_ARRAY_PARAMS_SIZE) {
$this->message = "Index '" . $index['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " attributes";
$this->message = "Index '" . $index['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " {$this->attributeTerm}s";
return false;
}
@@ -123,11 +145,11 @@ class Indexes extends Validator
$systemAttrs = ['$id', '$createdAt', '$updatedAt'];
foreach ($index['attributes'] as $attrIndex => $attr) {
if (!is_string($attr)) {
$this->message = "Invalid attribute at position $attrIndex in index '" . $index['key'] . "': must be a string";
$this->message = "Invalid {$this->attributeTerm} at position $attrIndex in index '" . $index['key'] . "': must be a string";
return false;
}
if (!$keyValidator->isValid($attr) && !in_array($attr, $systemAttrs)) {
$this->message = "Invalid attribute name '$attr' in index '" . $index['key'] . "'";
$this->message = "Invalid {$this->attributeTerm} name '$attr' in index '" . $index['key'] . "'";
return false;
}
}
@@ -142,7 +164,7 @@ class Indexes extends Validator
}
if (count($index['orders']) !== $attrCount) {
$this->message = "Index '" . $index['key'] . "': orders array length (" . count($index['orders']) . ") must match attributes array length ($attrCount)";
$this->message = "Index '" . $index['key'] . "': orders array length (" . count($index['orders']) . ") must match {$attributesFieldName} array length ($attrCount)";
return false;
}
@@ -162,7 +184,7 @@ class Indexes extends Validator
}
if (count($index['lengths']) !== $attrCount) {
$this->message = "Index '" . $index['key'] . "': lengths array length (" . count($index['lengths']) . ") must match attributes array length ($attrCount)";
$this->message = "Index '" . $index['key'] . "': lengths array length (" . count($index['lengths']) . ") must match {$attributesFieldName} array length ($attrCount)";
return false;
}
@@ -95,12 +95,9 @@ class ColumnIndex extends Model
public function filter(Document $document): Document
{
$columns = $document->getAttribute('attributes', []);
$document
->removeAttribute('attributes')
->setAttribute('columns', $columns);
$attributes = $document->getAttribute('attributes', []);
$document->setAttribute('columns', $attributes);
return $document;
}
}
@@ -86,6 +86,914 @@ trait DatabasesBase
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithStringAttribute(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestStringAttr',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals('TestStringAttr', $collection['body']['name']);
$this->assertCount(1, $collection['body']['attributes']);
$this->assertEquals('title', $collection['body']['attributes'][0]['key']);
$this->assertEquals('string', $collection['body']['attributes'][0]['type']);
$this->assertEquals(255, $collection['body']['attributes'][0]['size']);
$this->assertEquals(true, $collection['body']['attributes'][0]['required']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithMultipleAttributeTypes(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestMultiAttr',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'name',
'type' => 'string',
'size' => 100,
'required' => true,
],
[
'key' => 'age',
'type' => 'integer',
'required' => false,
'default' => 0,
'min' => 0,
'max' => 150,
],
[
'key' => 'score',
'type' => 'float',
'required' => false,
'default' => 0.0,
'min' => 0.0,
'max' => 100.0,
],
[
'key' => 'isActive',
'type' => 'boolean',
'required' => false,
'default' => true,
],
[
'key' => 'createdAt',
'type' => 'datetime',
'required' => false,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals('TestMultiAttr', $collection['body']['name']);
$this->assertCount(5, $collection['body']['attributes']);
// Verify each attribute type
$attributeKeys = array_column($collection['body']['attributes'], 'key');
$this->assertContains('name', $attributeKeys);
$this->assertContains('age', $attributeKeys);
$this->assertContains('score', $attributeKeys);
$this->assertContains('isActive', $attributeKeys);
$this->assertContains('createdAt', $attributeKeys);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithArrayAttribute(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestArrayAttr',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'tags',
'type' => 'string',
'size' => 50,
'required' => false,
'array' => true,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals('TestArrayAttr', $collection['body']['name']);
$this->assertCount(1, $collection['body']['attributes']);
$this->assertEquals('tags', $collection['body']['attributes'][0]['key']);
$this->assertEquals(true, $collection['body']['attributes'][0]['array']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithDefaultValues(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestDefaults',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'status',
'type' => 'string',
'size' => 20,
'required' => false,
'default' => 'pending',
],
[
'key' => 'count',
'type' => 'integer',
'required' => false,
'default' => 0,
],
[
'key' => 'enabled',
'type' => 'boolean',
'required' => false,
'default' => false,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(3, $collection['body']['attributes']);
// Find and verify default values
foreach ($collection['body']['attributes'] as $attr) {
if ($attr['key'] === 'status') {
$this->assertEquals('pending', $attr['default']);
}
if ($attr['key'] === 'count') {
$this->assertEquals(0, $attr['default']);
}
if ($attr['key'] === 'enabled') {
$this->assertEquals(false, $attr['default']);
}
}
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithKeyIndex(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestKeyIndex',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'title_idx',
'type' => 'key',
'attributes' => ['title'],
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(1, $collection['body']['attributes']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals('title_idx', $collection['body']['indexes'][0]['key']);
$this->assertEquals('key', $collection['body']['indexes'][0]['type']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithUniqueIndex(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestUniqueIndex',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'email',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'email_unique',
'type' => 'unique',
'attributes' => ['email'],
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals('email_unique', $collection['body']['indexes'][0]['key']);
$this->assertEquals('unique', $collection['body']['indexes'][0]['type']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithFulltextIndex(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestFulltextIndex',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'content',
'type' => 'string',
'size' => 10000,
'required' => false,
],
],
'indexes' => [
[
'key' => 'content_fulltext',
'type' => 'fulltext',
'attributes' => ['content'],
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals('content_fulltext', $collection['body']['indexes'][0]['key']);
$this->assertEquals('fulltext', $collection['body']['indexes'][0]['type']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithCompoundIndex(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestCompoundIndex',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'userId',
'type' => 'string',
'size' => 36,
'required' => true,
],
[
'key' => 'createdAt',
'type' => 'datetime',
'required' => true,
],
],
'indexes' => [
[
'key' => 'user_date_idx',
'type' => 'key',
'attributes' => ['userId', 'createdAt'],
'orders' => ['ASC', 'DESC'],
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(2, $collection['body']['attributes']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals('user_date_idx', $collection['body']['indexes'][0]['key']);
$this->assertCount(2, $collection['body']['indexes'][0]['attributes']);
$this->assertEquals(['ASC', 'DESC'], $collection['body']['indexes'][0]['orders']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithMultipleIndexes(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestMultiIndex',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
[
'key' => 'slug',
'type' => 'string',
'size' => 255,
'required' => true,
],
[
'key' => 'body',
'type' => 'string',
'size' => 50000,
'required' => false,
],
],
'indexes' => [
[
'key' => 'title_idx',
'type' => 'key',
'attributes' => ['title'],
],
[
'key' => 'slug_unique',
'type' => 'unique',
'attributes' => ['slug'],
],
[
'key' => 'body_fulltext',
'type' => 'fulltext',
'attributes' => ['body'],
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(3, $collection['body']['attributes']);
$this->assertCount(3, $collection['body']['indexes']);
$indexKeys = array_column($collection['body']['indexes'], 'key');
$this->assertContains('title_idx', $indexKeys);
$this->assertContains('slug_unique', $indexKeys);
$this->assertContains('body_fulltext', $indexKeys);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithEmptyArrays(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestEmptyArrays',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [],
'indexes' => [],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals('TestEmptyArrays', $collection['body']['name']);
$this->assertCount(0, $collection['body']['attributes']);
$this->assertCount(0, $collection['body']['indexes']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithIntegerMinMax(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestIntMinMax',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'temperature',
'type' => 'integer',
'required' => false,
'min' => -50,
'max' => 50,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(1, $collection['body']['attributes']);
$this->assertEquals('temperature', $collection['body']['attributes'][0]['key']);
$this->assertEquals(-50, $collection['body']['attributes'][0]['min']);
$this->assertEquals(50, $collection['body']['attributes'][0]['max']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithFloatMinMax(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestFloatMinMax',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'rating',
'type' => 'float',
'required' => false,
'min' => 0.0,
'max' => 5.0,
'default' => 0.0,
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(1, $collection['body']['attributes']);
$this->assertEquals('rating', $collection['body']['attributes'][0]['key']);
$this->assertEquals(0.0, $collection['body']['attributes'][0]['min']);
$this->assertEquals(5.0, $collection['body']['attributes'][0]['max']);
$this->assertEquals(0.0, $collection['body']['attributes'][0]['default']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionInvalidAttributeType(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestInvalidType',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'invalid',
'type' => 'invalid_type',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionInvalidIndexType(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestInvalidIndex',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'invalid_idx',
'type' => 'invalid_type',
'attributes' => ['title'],
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionMissingAttributeKey(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestMissingKey',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'type' => 'string',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionMissingAttributeType(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestMissingType',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'field',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionStringSizeMissing(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestMissingSize',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'required' => false,
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionIndexOnNonExistentAttribute(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestBadIndexAttr',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'nonexistent_idx',
'type' => 'key',
'attributes' => ['nonexistent_field'],
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionDuplicateAttributeKeys(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestDuplicateKeys',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
[
'key' => 'title',
'type' => 'string',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionDuplicateIndexKeys(array $data): void
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestDuplicateIndexKeys',
'documentSecurity' => true,
'permissions' => [],
'attributes' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'title_idx',
'type' => 'key',
'attributes' => ['title'],
],
[
'key' => 'title_idx',
'type' => 'unique',
'attributes' => ['title'],
],
],
]);
$this->assertEquals(400, $collection['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateCollectionWithIndexLengths(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'collectionId' => ID::unique(),
'name' => 'TestIndexLengths',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'attributes' => [
[
'key' => 'longText',
'type' => 'string',
'size' => 10000,
'required' => false,
],
],
'indexes' => [
[
'key' => 'text_idx',
'type' => 'key',
'attributes' => ['longText'],
'lengths' => [255],
],
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertCount(1, $collection['body']['indexes']);
$this->assertEquals([255], $collection['body']['indexes'][0]['lengths']);
return [
'databaseId' => $databaseId,
'collectionId' => $collection['body']['$id'],
];
}
/**
* @depends testCreateCollection
*/
@@ -86,6 +86,914 @@ trait DatabasesBase
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithStringColumn(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestStringCol',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertEquals('TestStringCol', $table['body']['name']);
$this->assertCount(1, $table['body']['columns']);
$this->assertEquals('title', $table['body']['columns'][0]['key']);
$this->assertEquals('string', $table['body']['columns'][0]['type']);
$this->assertEquals(255, $table['body']['columns'][0]['size']);
$this->assertEquals(true, $table['body']['columns'][0]['required']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithMultipleColumnTypes(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestMultiCol',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'name',
'type' => 'string',
'size' => 100,
'required' => true,
],
[
'key' => 'age',
'type' => 'integer',
'required' => false,
'default' => 0,
'min' => 0,
'max' => 150,
],
[
'key' => 'score',
'type' => 'float',
'required' => false,
'default' => 0.0,
'min' => 0.0,
'max' => 100.0,
],
[
'key' => 'isActive',
'type' => 'boolean',
'required' => false,
'default' => true,
],
[
'key' => 'createdAt',
'type' => 'datetime',
'required' => false,
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertEquals('TestMultiCol', $table['body']['name']);
$this->assertCount(5, $table['body']['columns']);
// Verify each column type
$columnKeys = array_column($table['body']['columns'], 'key');
$this->assertContains('name', $columnKeys);
$this->assertContains('age', $columnKeys);
$this->assertContains('score', $columnKeys);
$this->assertContains('isActive', $columnKeys);
$this->assertContains('createdAt', $columnKeys);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithArrayColumn(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestArrayCol',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'tags',
'type' => 'string',
'size' => 50,
'required' => false,
'array' => true,
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertEquals('TestArrayCol', $table['body']['name']);
$this->assertCount(1, $table['body']['columns']);
$this->assertEquals('tags', $table['body']['columns'][0]['key']);
$this->assertEquals(true, $table['body']['columns'][0]['array']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithDefaultValues(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestDefaults',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'status',
'type' => 'string',
'size' => 20,
'required' => false,
'default' => 'pending',
],
[
'key' => 'count',
'type' => 'integer',
'required' => false,
'default' => 0,
],
[
'key' => 'enabled',
'type' => 'boolean',
'required' => false,
'default' => false,
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(3, $table['body']['columns']);
// Find and verify default values
foreach ($table['body']['columns'] as $col) {
if ($col['key'] === 'status') {
$this->assertEquals('pending', $col['default']);
}
if ($col['key'] === 'count') {
$this->assertEquals(0, $col['default']);
}
if ($col['key'] === 'enabled') {
$this->assertEquals(false, $col['default']);
}
}
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithKeyIndex(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestKeyIndex',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'title_idx',
'type' => 'key',
'attributes' => ['title'],
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(1, $table['body']['columns']);
$this->assertCount(1, $table['body']['indexes']);
$this->assertEquals('title_idx', $table['body']['indexes'][0]['key']);
$this->assertEquals('key', $table['body']['indexes'][0]['type']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithUniqueIndex(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestUniqueIndex',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'email',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'email_unique',
'type' => 'unique',
'attributes' => ['email'],
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(1, $table['body']['indexes']);
$this->assertEquals('email_unique', $table['body']['indexes'][0]['key']);
$this->assertEquals('unique', $table['body']['indexes'][0]['type']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithFulltextIndex(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestFulltextIndex',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'content',
'type' => 'string',
'size' => 10000,
'required' => false,
],
],
'indexes' => [
[
'key' => 'content_fulltext',
'type' => 'fulltext',
'attributes' => ['content'],
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(1, $table['body']['indexes']);
$this->assertEquals('content_fulltext', $table['body']['indexes'][0]['key']);
$this->assertEquals('fulltext', $table['body']['indexes'][0]['type']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithCompoundIndex(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestCompoundIndex',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'userId',
'type' => 'string',
'size' => 36,
'required' => true,
],
[
'key' => 'createdAt',
'type' => 'datetime',
'required' => true,
],
],
'indexes' => [
[
'key' => 'user_date_idx',
'type' => 'key',
'attributes' => ['userId', 'createdAt'],
'orders' => ['ASC', 'DESC'],
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(2, $table['body']['columns']);
$this->assertCount(1, $table['body']['indexes']);
$this->assertEquals('user_date_idx', $table['body']['indexes'][0]['key']);
$this->assertCount(2, $table['body']['indexes'][0]['columns']);
$this->assertEquals(['ASC', 'DESC'], $table['body']['indexes'][0]['orders']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithMultipleIndexes(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestMultiIndex',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
[
'key' => 'slug',
'type' => 'string',
'size' => 255,
'required' => true,
],
[
'key' => 'body',
'type' => 'string',
'size' => 50000,
'required' => false,
],
],
'indexes' => [
[
'key' => 'title_idx',
'type' => 'key',
'attributes' => ['title'],
],
[
'key' => 'slug_unique',
'type' => 'unique',
'attributes' => ['slug'],
],
[
'key' => 'body_fulltext',
'type' => 'fulltext',
'attributes' => ['body'],
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(3, $table['body']['columns']);
$this->assertCount(3, $table['body']['indexes']);
$indexKeys = array_column($table['body']['indexes'], 'key');
$this->assertContains('title_idx', $indexKeys);
$this->assertContains('slug_unique', $indexKeys);
$this->assertContains('body_fulltext', $indexKeys);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithEmptyArrays(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestEmptyArrays',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [],
'indexes' => [],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertEquals('TestEmptyArrays', $table['body']['name']);
$this->assertCount(0, $table['body']['columns']);
$this->assertCount(0, $table['body']['indexes']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithIntegerMinMax(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestIntMinMax',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'temperature',
'type' => 'integer',
'required' => false,
'min' => -50,
'max' => 50,
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(1, $table['body']['columns']);
$this->assertEquals('temperature', $table['body']['columns'][0]['key']);
$this->assertEquals(-50, $table['body']['columns'][0]['min']);
$this->assertEquals(50, $table['body']['columns'][0]['max']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithFloatMinMax(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestFloatMinMax',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'rating',
'type' => 'float',
'required' => false,
'min' => 0.0,
'max' => 5.0,
'default' => 0.0,
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(1, $table['body']['columns']);
$this->assertEquals('rating', $table['body']['columns'][0]['key']);
$this->assertEquals(0.0, $table['body']['columns'][0]['min']);
$this->assertEquals(5.0, $table['body']['columns'][0]['max']);
$this->assertEquals(0.0, $table['body']['columns'][0]['default']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableInvalidColumnType(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestInvalidType',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'invalid',
'type' => 'invalid_type',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableInvalidIndexType(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestInvalidIndex',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'invalid_idx',
'type' => 'invalid_type',
'attributes' => ['title'],
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableMissingColumnKey(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestMissingKey',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'type' => 'string',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableMissingColumnType(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestMissingType',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'field',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableStringSizeMissing(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestMissingSize',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'required' => false,
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableIndexOnNonExistentColumn(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestBadIndexCol',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'nonexistent_idx',
'type' => 'key',
'attributes' => ['nonexistent_field'],
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableDuplicateColumnKeys(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestDuplicateKeys',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
[
'key' => 'title',
'type' => 'string',
'size' => 100,
'required' => false,
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableDuplicateIndexKeys(array $data): void
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestDuplicateIndexKeys',
'rowSecurity' => true,
'permissions' => [],
'columns' => [
[
'key' => 'title',
'type' => 'string',
'size' => 255,
'required' => true,
],
],
'indexes' => [
[
'key' => 'title_idx',
'type' => 'key',
'attributes' => ['title'],
],
[
'key' => 'title_idx',
'type' => 'unique',
'attributes' => ['title'],
],
],
]);
$this->assertEquals(400, $table['headers']['status-code']);
}
/**
* @depends testCreateDatabase
*/
public function testCreateTableWithIndexLengths(array $data): array
{
$databaseId = $data['databaseId'];
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'TestIndexLengths',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
'columns' => [
[
'key' => 'longText',
'type' => 'string',
'size' => 10000,
'required' => false,
],
],
'indexes' => [
[
'key' => 'text_idx',
'type' => 'key',
'attributes' => ['longText'],
'lengths' => [255],
],
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$this->assertCount(1, $table['body']['indexes']);
$this->assertEquals([255], $table['body']['indexes'][0]['lengths']);
return [
'databaseId' => $databaseId,
'tableId' => $table['body']['$id'],
];
}
/**
* @depends testCreateTable
*/