value)->toBe('app_v1'); expect($schema->toString())->toBe('app_v1'); }); it('creates default public schema', function () { $public = SchemaName::public(); expect($public->value)->toBe('public'); expect($public->isPublic())->toBeTrue(); }); it('creates information_schema', function () { $infoSchema = SchemaName::informationSchema(); expect($infoSchema->value)->toBe('information_schema'); expect($infoSchema->isSystemSchema())->toBeTrue(); }); it('creates pg_catalog', function () { $pgCatalog = SchemaName::pgCatalog(); expect($pgCatalog->value)->toBe('pg_catalog'); expect($pgCatalog->isSystemSchema())->toBeTrue(); }); it('validates schema name format', function () { // Valid names - should not throw SchemaName::fromString('app_v1'); SchemaName::fromString('my_schema'); SchemaName::fromString('_temp_schema'); SchemaName::fromString('schema123'); expect(true)->toBeTrue(); // Validation passed // Invalid names - should throw try { SchemaName::fromString(''); expect(false)->toBeTrue('Should have thrown for empty name'); } catch (\InvalidArgumentException $e) { expect($e->getMessage())->toContain('cannot be empty'); } try { SchemaName::fromString('123invalid'); expect(false)->toBeTrue('Should have thrown for name starting with number'); } catch (\InvalidArgumentException $e) { expect($e->getMessage())->toContain('must start with a letter or underscore'); } try { SchemaName::fromString('invalid-name'); expect(false)->toBeTrue('Should have thrown for name with hyphen'); } catch (\InvalidArgumentException $e) { expect($e->getMessage())->toContain('can only contain letters, numbers, and underscores'); } }); it('validates maximum length', function () { // Valid length $validName = str_repeat('a', 64); SchemaName::fromString($validName); // Should not throw // Too long $tooLong = str_repeat('a', 65); try { SchemaName::fromString($tooLong); expect(false)->toBeTrue('Should have thrown for name exceeding max length'); } catch (\InvalidArgumentException $e) { expect($e->getMessage())->toContain('exceeds maximum length'); } }); it('detects SQL injection attempts', function () { // Note: SQL injection attempts are caught by format validation try { SchemaName::fromString("app'; DROP SCHEMA--"); expect(false)->toBeTrue('Should have thrown for SQL injection attempt'); } catch (\InvalidArgumentException $e) { expect($e->getMessage())->toBeString(); } try { SchemaName::fromString('app UNION SELECT'); expect(false)->toBeTrue('Should have thrown for SQL injection attempt'); } catch (\InvalidArgumentException $e) { expect($e->getMessage())->toBeString(); } }); it('quotes schema names for different platforms', function () { $schema = SchemaName::fromString('app_v1'); expect($schema->quoted('postgresql'))->toBe('"app_v1"'); expect($schema->quoted('postgres'))->toBe('"app_v1"'); expect($schema->quoted('pgsql'))->toBe('"app_v1"'); expect($schema->quoted('sqlite'))->toBe('"app_v1"'); expect($schema->quoted('mysql'))->toBe('`app_v1`'); expect($schema->quoted())->toBe('"app_v1"'); // Default PostgreSQL }); it('qualifies table names', function () { $schema = SchemaName::fromString('app_v1'); $table = TableName::fromString('users'); expect($schema->qualifyTable($table, 'postgresql'))->toBe('"app_v1"."users"'); expect($schema->qualifyTable($table, 'mysql'))->toBe('`app_v1`.`users`'); }); it('compares schema names for equality', function () { $schema1 = SchemaName::fromString('app_v1'); $schema2 = SchemaName::fromString('app_v1'); $schema3 = SchemaName::fromString('app_v2'); expect($schema1->equals($schema2))->toBeTrue(); expect($schema1->equals($schema3))->toBeFalse(); // PostgreSQL schemas are case-sensitive $schemaLower = SchemaName::fromString('app'); $schemaUpper = SchemaName::fromString('APP'); expect($schemaLower->equals($schemaUpper))->toBeFalse(); }); it('matches schema name patterns', function () { $schema = SchemaName::fromString('app_v1'); expect($schema->matches('app_*'))->toBeTrue(); expect($schema->matches('*_v1'))->toBeTrue(); expect($schema->matches('app_v1'))->toBeTrue(); expect($schema->matches('other_*'))->toBeFalse(); }); it('detects reserved SQL keywords', function () { $schema = SchemaName::fromString('app_v1'); expect($schema->isReservedKeyword())->toBeFalse(); }); it('converts to lowercase', function () { $schema = SchemaName::fromString('App_V1'); expect($schema->toLower())->toBe('app_v1'); }); it('checks for schema name prefix', function () { $schema = SchemaName::fromString('app_v1'); expect($schema->hasPrefix('app_'))->toBeTrue(); expect($schema->hasPrefix('other_'))->toBeFalse(); }); it('checks for schema name suffix', function () { $schema = SchemaName::fromString('app_v1'); expect($schema->hasSuffix('_v1'))->toBeTrue(); expect($schema->hasSuffix('_v2'))->toBeFalse(); }); it('detects public schema', function () { $public = SchemaName::public(); expect($public->isPublic())->toBeTrue(); $custom = SchemaName::fromString('app_v1'); expect($custom->isPublic())->toBeFalse(); }); it('detects system schemas', function () { $infoSchema = SchemaName::informationSchema(); expect($infoSchema->isSystemSchema())->toBeTrue(); $pgCatalog = SchemaName::pgCatalog(); expect($pgCatalog->isSystemSchema())->toBeTrue(); $pgSchema = SchemaName::fromString('pg_temp'); expect($pgSchema->isSystemSchema())->toBeTrue(); $customSchema = SchemaName::fromString('app_v1'); expect($customSchema->isSystemSchema())->toBeFalse(); }); it('detects temporary schemas', function () { $tempSchema1 = SchemaName::fromString('temp_schema'); expect($tempSchema1->isTemporary())->toBeTrue(); $tempSchema2 = SchemaName::fromString('tmp_data'); expect($tempSchema2->isTemporary())->toBeTrue(); $tempSchema3 = SchemaName::fromString('schema_temp'); expect($tempSchema3->isTemporary())->toBeTrue(); $normalSchema = SchemaName::fromString('app_v1'); expect($normalSchema->isTemporary())->toBeFalse(); }); it('adds prefix to schema name', function () { $schema = SchemaName::fromString('v1'); $prefixed = $schema->withPrefix('app_'); expect($prefixed->value)->toBe('app_v1'); // Original unchanged (immutable) expect($schema->value)->toBe('v1'); }); it('removes prefix from schema name', function () { $schema = SchemaName::fromString('app_v1'); $unprefixed = $schema->withoutPrefix('app_'); expect($unprefixed->value)->toBe('v1'); // Removing non-existent prefix returns same instance $same = $unprefixed->withoutPrefix('other_'); expect($same->value)->toBe('v1'); }); it('converts to string via magic method', function () { $schema = SchemaName::fromString('app_v1'); expect((string) $schema)->toBe('app_v1'); }); it('is immutable', function () { $original = SchemaName::fromString('v1'); $prefixed = $original->withPrefix('app_'); // Original remains unchanged expect($original->value)->toBe('v1'); // New instance created - verify by value difference expect($prefixed->value)->toBe('app_v1'); // Removing non-existent prefix returns same instance (optimization) $unprefixed = $original->withoutPrefix('app_'); expect($unprefixed->value)->toBe('v1'); // But removing actual prefix creates new instance $prefixedSchema = SchemaName::fromString('app_v1'); $removedPrefix = $prefixedSchema->withoutPrefix('app_'); expect($removedPrefix->value)->toBe('v1'); expect($prefixedSchema->value)->toBe('app_v1'); // Original unchanged }); it('handles edge cases correctly', function () { // Single character (valid if letter or underscore) $a = SchemaName::fromString('a'); expect($a->value)->toBe('a'); $underscore = SchemaName::fromString('_'); expect($underscore->value)->toBe('_'); // Underscore prefix $schema = SchemaName::fromString('_temp'); expect($schema->value)->toBe('_temp'); // Numbers in name $schema = SchemaName::fromString('app_123'); expect($schema->value)->toBe('app_123'); }); it('factory methods create valid schemas', function () { SchemaName::public(); SchemaName::informationSchema(); SchemaName::pgCatalog(); expect(true)->toBeTrue(); // All factory methods succeeded }); });