Add attributes + indexes params

This commit is contained in:
Jake Barnby 2025-11-28 02:33:13 +13:00
parent 2bd308c236
commit 2a88341edc
5 changed files with 694 additions and 3 deletions

View file

@ -105,4 +105,64 @@ abstract class Action extends UtopiaAction
? Exception::COLLECTION_LIMIT_EXCEEDED
: Exception::TABLE_LIMIT_EXCEEDED;
}
/**
* Get the appropriate format unsupported exception.
*/
protected function getFormatUnsupportedException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_FORMAT_UNSUPPORTED
: Exception::COLUMN_FORMAT_UNSUPPORTED;
}
/**
* Get the correct default unsupported message.
*/
protected function getDefaultUnsupportedException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_DEFAULT_UNSUPPORTED
: Exception::COLUMN_DEFAULT_UNSUPPORTED;
}
/**
* Get the appropriate parent level not found exception.
*/
protected function getParentNotFoundException(): string
{
return $this->isCollectionsAPI()
? Exception::COLLECTION_NOT_FOUND
: Exception::TABLE_NOT_FOUND;
}
/**
* Get the correct invalid structure message.
*/
protected function getStructureException(): string
{
return $this->isCollectionsAPI()
? Exception::DOCUMENT_INVALID_STRUCTURE
: Exception::ROW_INVALID_STRUCTURE;
}
/**
* Get the exception for unknown attribute/column in index.
*/
protected function getParentUnknownException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_UNKNOWN
: Exception::COLUMN_UNKNOWN;
}
/**
* Get the exception for invalid attribute/column type in index.
*/
protected function getParentInvalidTypeException(): string
{
return $this->isCollectionsAPI()
? Exception::ATTRIBUTE_TYPE_INVALID
: Exception::COLUMN_TYPE_INVALID;
}
}

View file

@ -9,7 +9,9 @@ use Appwrite\SDK\ContentType;
use Appwrite\SDK\Deprecated;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Attributes as AttributesValidator;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Indexes as IndexesValidator;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Database;
use Utopia\Database\Document;
@ -17,10 +19,12 @@ use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\Index as IndexException;
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\Structure;
use Utopia\Database\Validator\UID;
use Utopia\Swoole\Response as SwooleResponse;
use Utopia\Validator\Boolean;
@ -75,13 +79,15 @@ class Create extends Action
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('documentSecurity', false, new Boolean(true), 'Enables configuring permissions for individual documents. A user needs one of document or collection level permissions to access a document. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is collection enabled? When set to \'disabled\', users cannot access the collection but Server SDKs with and API key can still read and write to the collection. No data is lost when this is toggled.', true)
->param('attributes', [], new AttributesValidator(), 'Array of attribute definitions to create. Each attribute should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
->param('indexes', [], new IndexesValidator(), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of attribute keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
public function action(string $databaseId, string $collectionId, string $name, ?array $permissions, bool $documentSecurity, bool $enabled, array $attributes, array $indexes, UtopiaResponse $response, Database $dbForProject, Event $queueForEvents): void
{
$database = Authorization::skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@ -113,20 +119,53 @@ class Create extends Action
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
$collectionAttributes = [];
$attributeDocuments = [];
foreach ($attributes as $attributeDef) {
$attrDoc = $this->buildAttributeDocument($database, $collection, $attributeDef, $dbForProject);
$collectionAttributes[] = $attrDoc['collection'];
$attributeDocuments[] = $attrDoc['document'];
}
$collectionIndexes = [];
$indexDocuments = [];
foreach ($indexes as $indexDef) {
$idxDoc = $this->buildIndexDocument($database, $collection, $indexDef, $collectionAttributes);
$collectionIndexes[] = $idxDoc['collection'];
$indexDocuments[] = $idxDoc['document'];
}
try {
$dbForProject->createCollection(
id: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
attributes: $collectionAttributes,
indexes: $collectionIndexes,
permissions: $permissions,
documentSecurity: $documentSecurity
);
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
} catch (IndexException) {
throw new Exception($this->getInvalidIndexException());
} catch (IndexException $e) {
throw new Exception($this->getInvalidIndexException(), $e->getMessage());
} catch (LimitException) {
throw new Exception($this->getLimitException());
}
// Create documents in attributes and indexes collections
try {
if (!empty($attributeDocuments)) {
$dbForProject->createDocuments('attributes', $attributeDocuments);
}
if (!empty($indexDocuments)) {
$dbForProject->createDocuments('indexes', $indexDocuments);
}
} catch (DuplicateException) {
throw new Exception($this->getDuplicateException());
}
$dbForProject->purgeCachedDocument('database_' . $database->getSequence(), $collection->getId());
$dbForProject->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForEvents
->setContext('database', $database)
->setParam('databaseId', $databaseId)
@ -136,4 +175,191 @@ class Create extends Action
->setStatusCode(SwooleResponse::STATUS_CODE_CREATED)
->dynamic($collection, $this->getResponseModel());
}
/**
* Build attribute Document objects from a definition array
*
* @return array{collection: Document, document: Document}
*/
protected function buildAttributeDocument(Document $database, Document $collection, array $attributeDef, Database $dbForProject): array
{
$key = $attributeDef['key'];
$type = $attributeDef['type'];
$size = $attributeDef['size'] ?? 0;
$required = $attributeDef['required'] ?? false;
$signed = $attributeDef['signed'] ?? true;
$array = $attributeDef['array'] ?? false;
$format = $attributeDef['format'] ?? '';
$formatOptions = [];
$filters = $attributeDef['filters'] ?? [];
$default = $attributeDef['default'] ?? null;
$options = [];
if ($type === Database::VAR_STRING) {
if ($size === 0) {
$size = 256; // Default size for strings
}
}
if ($format === APP_DATABASE_ATTRIBUTE_ENUM && isset($attributeDef['elements'])) {
$formatOptions = ['elements' => $attributeDef['elements']];
}
if (isset($attributeDef['min']) && isset($attributeDef['max'])) {
$format = $type === Database::VAR_INTEGER ? APP_DATABASE_ATTRIBUTE_INT_RANGE : APP_DATABASE_ATTRIBUTE_FLOAT_RANGE;
$formatOptions = [
'min' => $attributeDef['min'],
'max' => $attributeDef['max'],
];
}
if ($type === Database::VAR_RELATIONSHIP) {
$options = [
'relatedCollection' => $attributeDef['relatedCollection'] ?? '',
'relationType' => $attributeDef['relationType'] ?? Database::RELATION_ONE_TO_ONE,
'twoWay' => $attributeDef['twoWay'] ?? false,
'twoWayKey' => $attributeDef['twoWayKey'] ?? '',
'onDelete' => $attributeDef['onDelete'] ?? Database::RELATION_MUTATE_RESTRICT,
];
}
if (!empty($format)) {
if (!Structure::hasFormat($format, $type)) {
throw new Exception($this->getFormatUnsupportedException(), "Format $format not available for $type attributes.");
}
}
if ($required && isset($default) && $default !== null) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for required ' . $this->getContext());
}
if ($array && isset($default) && $default !== null) {
throw new Exception($this->getDefaultUnsupportedException(), 'Cannot set default value for array ' . $this->getContext() . 's');
}
if ($type === Database::VAR_RELATIONSHIP) {
$options['side'] = Database::RELATION_SIDE_PARENT;
$relatedCollection = $dbForProject->getDocument('database_' . $database->getSequence(), $options['relatedCollection'] ?? '');
if ($relatedCollection->isEmpty()) {
$parent = $this->isCollectionsAPI() ? 'collection' : 'table';
throw new Exception($this->getParentNotFoundException(), "The related $parent was not found.");
}
}
$collectionDoc = new Document([
'$id' => $key,
'key' => $key,
'type' => $type,
'size' => $size,
'required' => $required,
'signed' => $signed,
'default' => $default,
'array' => $array,
'format' => $format,
'formatOptions' => $formatOptions,
'filters' => $filters,
'options' => $options,
]);
$document = new Document([
'$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'databaseInternalId' => $database->getSequence(),
'databaseId' => $database->getId(),
'collectionInternalId' => $collection->getSequence(),
'collectionId' => $collection->getId(),
'type' => $type,
'status' => 'available',
'size' => $size,
'required' => $required,
'signed' => $signed,
'default' => $default,
'array' => $array,
'format' => $format,
'formatOptions' => $formatOptions,
'filters' => $filters,
'options' => $options,
]);
return [
'collection' => $collectionDoc,
'document' => $document,
];
}
/**
* Build index Document objects from a definition array
*
* @return array{collection: Document, document: Document}
*/
protected function buildIndexDocument(Document $database, Document $collection, array $indexDef, array $attributeDocuments): array
{
$key = $indexDef['key'];
$type = $indexDef['type'];
$indexAttributes = $indexDef['attributes'];
$orders = $indexDef['orders'] ?? [];
$lengths = $indexDef['lengths'] ?? [];
$attrKeys = array_map(fn ($a) => $a->getAttribute('key'), $attributeDocuments);
$systemAttrs = ['$id', '$createdAt', '$updatedAt'];
foreach ($indexAttributes as $i => $attr) {
if (!in_array($attr, $attrKeys) && !in_array($attr, $systemAttrs)) {
throw new Exception($this->getParentUnknownException(), "Unknown attribute: " . $attr . ". Verify the attribute name or ensure it's in the attributes list.");
}
$attrIndex = array_search($attr, $attrKeys);
if ($attrIndex !== false) {
$attrDoc = $attributeDocuments[$attrIndex];
$attrType = $attrDoc->getAttribute('type');
$attrArray = $attrDoc->getAttribute('array', false);
if ($attrType === Database::VAR_RELATIONSHIP) {
throw new Exception($this->getParentInvalidTypeException(), "Cannot create an index for a relationship attribute: " . $attr);
}
if (empty($lengths[$i])) {
$lengths[$i] = null;
}
if ($attrArray === true) {
$lengths[$i] = Database::MAX_ARRAY_INDEX_LENGTH;
$orders[$i] = null;
}
} else {
if (empty($lengths[$i])) {
$lengths[$i] = null;
}
}
}
$collectionDoc = new Document([
'$id' => $key,
'key' => $key,
'type' => $type,
'attributes' => $indexAttributes,
'lengths' => $lengths,
'orders' => $orders,
]);
$document = new Document([
'$id' => ID::custom($database->getSequence() . '_' . $collection->getSequence() . '_' . $key),
'key' => $key,
'status' => 'available',
'databaseInternalId' => $database->getSequence(),
'databaseId' => $database->getId(),
'collectionInternalId' => $collection->getSequence(),
'collectionId' => $collection->getId(),
'type' => $type,
'attributes' => $indexAttributes,
'lengths' => $lengths,
'orders' => $orders,
]);
return [
'collection' => $collectionDoc,
'document' => $document,
];
}
}

View file

@ -7,7 +7,9 @@ use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Database\Validator\Attributes as AttributesValidator;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Indexes as IndexesValidator;
use Appwrite\Utopia\Response as UtopiaResponse;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
@ -60,6 +62,8 @@ class Create extends CollectionCreate
->param('permissions', null, new Nullable(new Permissions(APP_LIMIT_ARRAY_PARAMS_SIZE)), 'An array of permissions strings. By default, no user is granted with any permissions. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('rowSecurity', false, new Boolean(true), 'Enables configuring permissions for individual rows. A user needs one of row or table level permissions to access a row. [Learn more about permissions](https://appwrite.io/docs/permissions).', true)
->param('enabled', true, new Boolean(), 'Is table enabled? When set to \'disabled\', users cannot access the table but Server SDKs with and API key can still read and write to the table. No data is lost when this is toggled.', true)
->param('columns', [], new AttributesValidator(), 'Array of column definitions to create. Each column should contain: key (string), type (string: string, integer, float, boolean, datetime, relationship), size (integer, required for string type), required (boolean, optional), default (mixed, optional), array (boolean, optional), and type-specific options.', true)
->param('indexes', [], new IndexesValidator(), 'Array of index definitions to create. Each index should contain: key (string), type (string: key, fulltext, unique, spatial), attributes (array of column keys), orders (array of ASC/DESC, optional), and lengths (array of integers, optional).', true)
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')

View file

@ -0,0 +1,211 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Validator;
class Attributes extends Validator
{
protected int $maxAttributes;
protected string $message = 'Invalid attributes';
/**
* @var array<string> Supported attribute types
*/
protected array $supportedTypes = [
Database::VAR_STRING,
Database::VAR_INTEGER,
Database::VAR_FLOAT,
Database::VAR_BOOLEAN,
Database::VAR_DATETIME,
Database::VAR_RELATIONSHIP,
Database::VAR_POINT,
Database::VAR_LINESTRING,
Database::VAR_POLYGON,
];
/**
* @var array<string> Supported formats for string attributes
*/
protected array $supportedFormats = [
'',
APP_DATABASE_ATTRIBUTE_EMAIL,
APP_DATABASE_ATTRIBUTE_ENUM,
APP_DATABASE_ATTRIBUTE_IP,
APP_DATABASE_ATTRIBUTE_URL,
];
/**
* @param int $maxAttributes Maximum number of attributes allowed
*/
public function __construct(int $maxAttributes = APP_LIMIT_ARRAY_PARAMS_SIZE)
{
$this->maxAttributes = $maxAttributes;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
if (!is_array($value)) {
$this->message = 'Attributes must be an array';
return false;
}
if (count($value) > $this->maxAttributes) {
$this->message = 'Maximum of ' . $this->maxAttributes . ' attributes allowed';
return false;
}
$keyValidator = new Key();
$keys = [];
foreach ($value as $index => $attribute) {
if (!is_array($attribute)) {
$this->message = "Attribute 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'";
return false;
}
if (!isset($attribute['type'])) {
$this->message = "Attribute 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();
return false;
}
// Check for duplicate keys
if (in_array($attribute['key'], $keys)) {
$this->message = "Duplicate attribute key: " . $attribute['key'];
return false;
}
$keys[] = $attribute['key'];
// Validate type
if (!in_array($attribute['type'], $this->supportedTypes)) {
$this->message = "Invalid type for attribute '" . $attribute['key'] . "': " . $attribute['type'];
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;
return false;
}
}
// Validate format if provided
if (isset($attribute['format']) && $attribute['format'] !== '') {
if (!in_array($attribute['format'], $this->supportedFormats)) {
$this->message = "Invalid format for attribute '" . $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";
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";
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";
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";
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";
return false;
}
// 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";
return false;
}
}
// Validate relationship options
if ($attribute['type'] === Database::VAR_RELATIONSHIP) {
if (!isset($attribute['relatedCollection']) || empty($attribute['relatedCollection'])) {
$this->message = "Relationship attribute '" . $attribute['key'] . "' must have 'relatedCollection'";
return false;
}
if (!isset($attribute['relationType']) || !in_array($attribute['relationType'], [
Database::RELATION_ONE_TO_ONE,
Database::RELATION_ONE_TO_MANY,
Database::RELATION_MANY_TO_ONE,
Database::RELATION_MANY_TO_MANY,
])) {
$this->message = "Relationship attribute '" . $attribute['key'] . "' must have valid 'relationType'";
return false;
}
}
}
return true;
}
/**
* Is array
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}

View file

@ -0,0 +1,190 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Database;
use Utopia\Database\Validator\Key;
use Utopia\Validator;
class Indexes extends Validator
{
protected int $maxIndexes;
protected string $message = 'Invalid indexes';
/**
* @var array<string> Supported index types
*/
protected array $supportedTypes = [
Database::INDEX_KEY,
Database::INDEX_FULLTEXT,
Database::INDEX_UNIQUE,
Database::INDEX_SPATIAL,
];
/**
* @var array<string> Supported orders
*/
protected array $supportedOrders = [
Database::ORDER_ASC,
Database::ORDER_DESC,
];
/**
* @param int $maxIndexes Maximum number of indexes allowed
*/
public function __construct(int $maxIndexes = APP_LIMIT_ARRAY_PARAMS_SIZE)
{
$this->maxIndexes = $maxIndexes;
}
/**
* Get Description
*
* Returns validator description
*
* @return string
*/
public function getDescription(): string
{
return $this->message;
}
/**
* Is valid
*
* @param mixed $value
* @return bool
*/
public function isValid($value): bool
{
if (!is_array($value)) {
$this->message = 'Indexes must be an array';
return false;
}
if (count($value) > $this->maxIndexes) {
$this->message = 'Maximum of ' . $this->maxIndexes . ' indexes allowed';
return false;
}
$keyValidator = new Key();
$keys = [];
foreach ($value as $index => $indexDef) {
if (!is_array($indexDef)) {
$this->message = "Index at position $index must be an object";
return false;
}
// Validate required fields
if (!isset($indexDef['key'])) {
$this->message = "Index at position $index is missing required field 'key'";
return false;
}
if (!isset($indexDef['type'])) {
$this->message = "Index at position $index is missing required field 'type'";
return false;
}
if (!isset($indexDef['attributes']) || !is_array($indexDef['attributes'])) {
$this->message = "Index at position $index is missing required field 'attributes' (must be an array)";
return false;
}
// Validate key
if (!$keyValidator->isValid($indexDef['key'])) {
$this->message = "Invalid key for index at position $index: " . $keyValidator->getDescription();
return false;
}
// Check for duplicate keys
if (in_array($indexDef['key'], $keys)) {
$this->message = "Duplicate index key: " . $indexDef['key'];
return false;
}
$keys[] = $indexDef['key'];
// Validate type
if (!in_array($indexDef['type'], $this->supportedTypes)) {
$this->message = "Invalid type for index '" . $indexDef['key'] . "': " . $indexDef['type'];
return false;
}
// Validate attributes array
if (empty($indexDef['attributes'])) {
$this->message = "Index '" . $indexDef['key'] . "' must have at least one attribute";
return false;
}
if (count($indexDef['attributes']) > APP_LIMIT_ARRAY_PARAMS_SIZE) {
$this->message = "Index '" . $indexDef['key'] . "' cannot have more than " . APP_LIMIT_ARRAY_PARAMS_SIZE . " attributes";
return false;
}
// Validate each attribute in the index
foreach ($indexDef['attributes'] as $attrIndex => $attr) {
if (!is_string($attr)) {
$this->message = "Invalid attribute at position $attrIndex in index '" . $indexDef['key'] . "': must be a string";
return false;
}
if (!$keyValidator->isValid($attr) && !in_array($attr, ['$id', '$createdAt', '$updatedAt'])) {
$this->message = "Invalid attribute name '$attr' in index '" . $indexDef['key'] . "'";
return false;
}
}
// Validate orders if provided
if (isset($indexDef['orders'])) {
if (!is_array($indexDef['orders'])) {
$this->message = "Index '" . $indexDef['key'] . "' orders must be an array";
return false;
}
foreach ($indexDef['orders'] as $order) {
if ($order !== null && $order !== '' && !in_array($order, $this->supportedOrders)) {
$this->message = "Invalid order '$order' in index '" . $indexDef['key'] . "'. Must be 'ASC' or 'DESC'";
return false;
}
}
}
// Validate lengths if provided
if (isset($indexDef['lengths'])) {
if (!is_array($indexDef['lengths'])) {
$this->message = "Index '" . $indexDef['key'] . "' lengths must be an array";
return false;
}
foreach ($indexDef['lengths'] as $length) {
if ($length !== null && (!is_int($length) || $length < 0)) {
$this->message = "Invalid length in index '" . $indexDef['key'] . "': must be a non-negative integer or null";
return false;
}
}
}
}
return true;
}
/**
* Is array
*
* @return bool
*/
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return self::TYPE_ARRAY;
}
}