value)->toBe('idx_users_email'); expect($index->toString())->toBe('idx_users_email'); }); it('creates PRIMARY KEY index', function () { $primary = IndexName::primary(); expect($primary->value)->toBe('PRIMARY'); expect($primary->isPrimary())->toBeTrue(); }); it('validates index name format', function () { // Valid names expect(fn() => IndexName::fromString('idx_users_email'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::fromString('unique_users_email'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::fromString('_temp_index'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::fromString('index123'))->not->toThrow(\InvalidArgumentException::class); // Invalid names expect(fn() => IndexName::fromString('')) ->toThrow(\InvalidArgumentException::class, 'cannot be empty'); expect(fn() => IndexName::fromString('123invalid')) ->toThrow(\InvalidArgumentException::class, 'must start with a letter or underscore'); expect(fn() => IndexName::fromString('invalid-name')) ->toThrow(\InvalidArgumentException::class, 'can only contain letters, numbers, and underscores'); }); it('allows PRIMARY as special case', function () { // PRIMARY is always valid expect(fn() => IndexName::fromString('PRIMARY'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::fromString('primary'))->not->toThrow(\InvalidArgumentException::class); }); it('validates maximum length', function () { $validName = str_repeat('a', 64); expect(fn() => IndexName::fromString($validName))->not->toThrow(\InvalidArgumentException::class); $tooLong = str_repeat('a', 65); expect(fn() => IndexName::fromString($tooLong)) ->toThrow(\InvalidArgumentException::class, 'exceeds maximum length'); }); it('detects SQL injection attempts', function () { // Note: SQL injection attempts are caught by format validation // since they contain invalid characters (quotes, hyphens, spaces, etc.) expect(fn() => IndexName::fromString("idx'; DROP TABLE--")) ->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::fromString('idx UNION SELECT')) ->toThrow(\InvalidArgumentException::class); }); it('quotes index names for different platforms', function () { $index = IndexName::fromString('idx_users_email'); expect($index->quoted('mysql'))->toBe('`idx_users_email`'); expect($index->quoted('postgresql'))->toBe('"idx_users_email"'); expect($index->quoted('postgres'))->toBe('"idx_users_email"'); expect($index->quoted('pgsql'))->toBe('"idx_users_email"'); expect($index->quoted('sqlite'))->toBe('"idx_users_email"'); expect($index->quoted())->toBe('`idx_users_email`'); // Default MySQL }); it('never quotes PRIMARY KEY', function () { $primary = IndexName::primary(); expect($primary->quoted('mysql'))->toBe('PRIMARY KEY'); expect($primary->quoted('postgresql'))->toBe('PRIMARY KEY'); expect($primary->quoted('sqlite'))->toBe('PRIMARY KEY'); }); it('compares index names for equality', function () { $idx1 = IndexName::fromString('idx_users_email'); $idx2 = IndexName::fromString('idx_users_email'); $idx3 = IndexName::fromString('IDX_USERS_EMAIL'); // Different case $idx4 = IndexName::fromString('idx_orders_id'); expect($idx1->equals($idx2))->toBeTrue(); expect($idx1->equals($idx3))->toBeTrue(); // Case-insensitive expect($idx1->equals($idx4))->toBeFalse(); }); it('matches index name patterns', function () { $index = IndexName::fromString('idx_users_email'); expect($index->matches('idx_*'))->toBeTrue(); expect($index->matches('*_email'))->toBeTrue(); expect($index->matches('idx_users_*'))->toBeTrue(); expect($index->matches('unique_*'))->toBeFalse(); }); it('detects reserved SQL keywords', function () { $index = IndexName::fromString('idx_users_email'); expect($index->isReservedKeyword())->toBeFalse(); }); it('converts to lowercase', function () { $index = IndexName::fromString('IDX_Users_Email'); expect($index->toLower())->toBe('idx_users_email'); }); it('checks for index name prefix', function () { $index = IndexName::fromString('idx_users_email'); expect($index->hasPrefix('idx_'))->toBeTrue(); expect($index->hasPrefix('unique_'))->toBeFalse(); }); it('checks for index name suffix', function () { $index = IndexName::fromString('idx_users_email'); expect($index->hasSuffix('_email'))->toBeTrue(); expect($index->hasSuffix('_id'))->toBeFalse(); }); it('detects unique indexes', function () { $uniqueIdx1 = IndexName::fromString('unique_users_email'); $uniqueIdx2 = IndexName::fromString('idx_users_email_unique'); $uniqueIdx3 = IndexName::fromString('idx_unique_constraint_users'); $normalIdx = IndexName::fromString('idx_users_email'); expect($uniqueIdx1->isUniqueIndex())->toBeTrue(); expect($uniqueIdx2->isUniqueIndex())->toBeTrue(); expect($uniqueIdx3->isUniqueIndex())->toBeTrue(); expect($normalIdx->isUniqueIndex())->toBeFalse(); }); it('detects full-text indexes', function () { $fulltextIdx1 = IndexName::fromString('fulltext_posts_content'); $fulltextIdx2 = IndexName::fromString('idx_posts_content_fulltext'); $fulltextIdx3 = IndexName::fromString('idx_fulltext_search'); $normalIdx = IndexName::fromString('idx_posts_title'); expect($fulltextIdx1->isFullTextIndex())->toBeTrue(); expect($fulltextIdx2->isFullTextIndex())->toBeTrue(); expect($fulltextIdx3->isFullTextIndex())->toBeTrue(); expect($normalIdx->isFullTextIndex())->toBeFalse(); }); it('generates conventional index names for columns', function () { $table = TableName::fromString('users'); $emailColumn = ColumnName::fromString('email'); $index = IndexName::forColumns($table, $emailColumn); expect($index->value)->toBe('idx_users_email'); expect($index->hasPrefix('idx_'))->toBeTrue(); }); it('generates multi-column index names', function () { $table = TableName::fromString('users'); $firstNameColumn = ColumnName::fromString('first_name'); $lastNameColumn = ColumnName::fromString('last_name'); $index = IndexName::forColumns($table, $firstNameColumn, $lastNameColumn); expect($index->value)->toBe('idx_users_first_name_last_name'); }); it('generates unique constraint names', function () { $table = TableName::fromString('users'); $emailColumn = ColumnName::fromString('email'); $index = IndexName::uniqueFor($table, $emailColumn); expect($index->value)->toBe('unique_users_email'); expect($index->isUniqueIndex())->toBeTrue(); }); it('generates multi-column unique constraint names', function () { $table = TableName::fromString('users'); $emailColumn = ColumnName::fromString('email'); $tenantIdColumn = ColumnName::fromString('tenant_id'); $index = IndexName::uniqueFor($table, $emailColumn, $tenantIdColumn); expect($index->value)->toBe('unique_users_email_tenant_id'); expect($index->isUniqueIndex())->toBeTrue(); }); it('generates foreign key index names', function () { $table = TableName::fromString('orders'); $userIdColumn = ColumnName::fromString('user_id'); $index = IndexName::foreignKeyFor($table, $userIdColumn); expect($index->value)->toBe('fk_orders_user_id'); expect($index->hasPrefix('fk_'))->toBeTrue(); }); it('converts to string via magic method', function () { $index = IndexName::fromString('idx_users_email'); expect((string) $index)->toBe('idx_users_email'); }); it('is immutable', function () { $index = IndexName::fromString('idx_users_email'); $value = $index->value; // Value cannot be changed expect($index->value)->toBe('idx_users_email'); expect($value)->toBe('idx_users_email'); }); it('handles edge cases correctly', function () { // Single character (valid if letter or underscore) expect(fn() => IndexName::fromString('a'))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::fromString('_'))->not->toThrow(\InvalidArgumentException::class); // Underscore prefix $index = IndexName::fromString('_temp_index'); expect($index->value)->toBe('_temp_index'); // Numbers in name $index = IndexName::fromString('idx_123'); expect($index->value)->toBe('idx_123'); }); it('detects PRIMARY in different cases', function () { $primary1 = IndexName::fromString('PRIMARY'); $primary2 = IndexName::fromString('primary'); $primary3 = IndexName::fromString('Primary'); expect($primary1->isPrimary())->toBeTrue(); expect($primary2->isPrimary())->toBeTrue(); expect($primary3->isPrimary())->toBeTrue(); }); it('factory methods create valid index names', function () { $table = TableName::fromString('users'); $emailColumn = ColumnName::fromString('email'); // All factory methods should produce valid names expect(fn() => IndexName::forColumns($table, $emailColumn))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::uniqueFor($table, $emailColumn))->not->toThrow(\InvalidArgumentException::class); expect(fn() => IndexName::foreignKeyFor($table, $emailColumn))->not->toThrow(\InvalidArgumentException::class); }); it('combines with TableName and ColumnName correctly', function () { $table = TableName::fromString('users'); $emailColumn = ColumnName::fromString('email'); $statusColumn = ColumnName::fromString('status'); $index = IndexName::forColumns($table, $emailColumn, $statusColumn); expect($index->value)->toContain('users'); expect($index->value)->toContain('email'); expect($index->value)->toContain('status'); }); });