diff --git a/oracle/migrator.go b/oracle/migrator.go index b1d8b54..10d5aa4 100644 --- a/oracle/migrator.go +++ b/oracle/migrator.go @@ -484,6 +484,81 @@ func (m Migrator) DropConstraint(value interface{}, name string) error { return m.Migrator.DropConstraint(value, name) } +// CreateType creates or replaces an Oracle user-defined type +func (m Migrator) CreateType(typeName string, args ...string) error { + typeName = strings.TrimSpace(typeName) + if typeName == "" { + return fmt.Errorf("typeName is required") + } + var typeKind, typeOf string + if len(args) > 0 { + typeKind = args[0] + } + if len(args) > 1 { + typeOf = args[1] + } + + name := strings.ToLower(typeName) + typeKind = strings.TrimSpace(typeKind) + typeOf = strings.TrimSpace(typeOf) + + // Incomplete object type + if typeKind == "" && typeOf == "" { + ddl := fmt.Sprintf(`CREATE TYPE "%s"`, name) + return m.DB.Exec(ddl).Error + } + + k := strings.ToUpper(typeKind) + var ddl string + + switch { + // Standalone varying array (varray) type and Standalone nested table type + case strings.HasPrefix(k, "VARRAY") || strings.HasPrefix(k, "TABLE "): + if typeOf == "" { + return fmt.Errorf("typeof is required for collection types (VARRAY/TABLE)") + } + ddl = fmt.Sprintf(`CREATE OR REPLACE TYPE "%s" AS %s OF %s`, name, typeKind, typeOf) + + // Abstract Data Type (ADT) + case k == "OBJECT" || strings.HasPrefix(k, "OBJECT"): + if typeOf == "" { + return fmt.Errorf("attributes definition is required for OBJECT types") + } + attrs := typeOf + if !strings.HasPrefix(attrs, "(") { + attrs = "(" + attrs + ")" + } + ddl = fmt.Sprintf(`CREATE OR REPLACE TYPE "%s" AS OBJECT %s`, name, attrs) + + default: + // Invalid or unsupported types + return fmt.Errorf("unsupported type kind %q (must be OBJECT, VARRAY, or TABLE)", typeKind) + } + + return m.DB.Exec(ddl).Error +} + +// DropType drops a user-defined type +func (m Migrator) DropType(typeName string) error { + typeName = strings.TrimSpace(typeName) + if typeName == "" { + return fmt.Errorf("dropType: typeName is required") + } + ddl := fmt.Sprintf(`DROP TYPE "%s" FORCE`, strings.ToLower(typeName)) + return m.DB.Exec(ddl).Error +} + +// HasType checks whether a user-defined type exists +func (m Migrator) HasType(typeName string) bool { + if typeName == "" { + return false + } + + var count int + err := m.DB.Raw(`SELECT COUNT(*) FROM USER_TYPES WHERE UPPER(TYPE_NAME) = UPPER(?)`, typeName).Scan(&count).Error + return err == nil && count > 0 +} + // DropIndex drops the index with the specified `name` from the table associated with `value` func (m Migrator) DropIndex(value interface{}, name string) error { return m.RunWithValue(value, func(stmt *gorm.Statement) error { diff --git a/tests/migrate_test.go b/tests/migrate_test.go index 2da7589..3e4f694 100644 --- a/tests/migrate_test.go +++ b/tests/migrate_test.go @@ -50,6 +50,7 @@ import ( "time" + "github.com/oracle-samples/gorm-oracle/oracle" . "github.com/oracle-samples/gorm-oracle/tests/utils" "github.com/stretchr/testify/assert" @@ -1970,6 +1971,174 @@ func TestOracleSequences(t *testing.T) { } } +func TestOracleTypeCreateDrop(t *testing.T) { + if DB.Dialector.Name() != "oracle" { + t.Skip("Skipping Oracle type test: not running on Oracle") + } + + const ( + typeName = "email_list" + tableName = "email_varray_tab" + + objectTypeName = "person_obj" + objectTableName = "person_obj_tab" + + incompleteTypeName = "department_t" + unsupportedTypeName = "unsupported_type_t" + ) + + // Assert that DB.Migrator() is an oracle.Migrator (so we can use Oracle-specific methods) + m, ok := DB.Migrator().(oracle.Migrator) + if !ok { + t.Skip("Skipping: current dialect migrator is not Oracle-specific") + } + + // Drop types if they exist + t.Run("drop_existing_types_if_any", func(t *testing.T) { + if err := m.DropType(typeName); err != nil && !strings.Contains(err.Error(), "ORA-04043") { + t.Fatalf("Unexpected error dropping type %s: %v", typeName, err) + } + if err := m.DropType(objectTypeName); err != nil && !strings.Contains(err.Error(), "ORA-04043") { + t.Fatalf("Unexpected error dropping type %s: %v", objectTypeName, err) + } + if err := m.DropType(incompleteTypeName); err != nil && !strings.Contains(err.Error(), "ORA-04043") { + t.Fatalf("Unexpected error dropping type %s: %v", incompleteTypeName, err) + } + }) + + // Create new VARRAY type + t.Run("create_varray_type", func(t *testing.T) { + err := m.CreateType(typeName, "VARRAY(10)", "VARCHAR2(60)") + if err != nil { + t.Fatalf("Failed to create Oracle VARRAY type: %v", err) + } + + // Verify it exists via HasType + if !m.HasType(typeName) { + t.Fatalf("Expected Oracle VARRAY type %s to exist", typeName) + } + }) + + // Create table using the VARRAY type + t.Run("create_table_using_varray_type", func(t *testing.T) { + createTableSQL := fmt.Sprintf(` + CREATE TABLE "%s" ( + "ID" NUMBER PRIMARY KEY, + "EMAILS" "%s" + )`, tableName, typeName) + + if err := DB.Exec(createTableSQL).Error; err != nil { + t.Fatalf("Failed to create table using type %s: %v", typeName, err) + } + + // Verify table exists + if !m.HasTable(tableName) { + t.Fatalf("Expected table %s to exist", tableName) + } + }) + + // Create ADT (OBJECT) type + t.Run("create_object_type", func(t *testing.T) { + err := m.CreateType(objectTypeName, "OBJECT", ` + first_name VARCHAR2(50), + last_name VARCHAR2(50), + age NUMBER + `) + if err != nil { + t.Fatalf("Failed to create Oracle OBJECT type: %v", err) + } + + // Verify it exists via HasType + if !m.HasType(objectTypeName) { + t.Fatalf("Expected Oracle OBJECT type %s to exist", objectTypeName) + } + }) + + // Create table using the OBJECT type + t.Run("create_table_using_object_type", func(t *testing.T) { + createTableSQL := fmt.Sprintf(` + CREATE TABLE "%s" ( + "ID" NUMBER PRIMARY KEY, + "PERSON" "%s" + )`, objectTableName, objectTypeName) + + if err := DB.Exec(createTableSQL).Error; err != nil { + t.Fatalf("Failed to create table using object type %s: %v", objectTypeName, err) + } + + // Verify table exists + if !m.HasTable(objectTableName) { + t.Fatalf("Expected table %s to exist", objectTableName) + } + }) + + // Create incomplete type (forward declaration) + t.Run("create_incomplete_type", func(t *testing.T) { + if err := m.CreateType(incompleteTypeName); err != nil { + t.Fatalf("Failed to create incomplete type %s: %v", incompleteTypeName, err) + } + if !m.HasType(incompleteTypeName) { + t.Fatalf("Expected incomplete type %s to exist", incompleteTypeName) + } + if err := m.DropType(incompleteTypeName); err != nil { + t.Fatalf("Failed to drop incomplete type %s: %v", incompleteTypeName, err) + } + if m.HasType(incompleteTypeName) { + t.Fatalf("Expected incomplete type %s to be dropped", incompleteTypeName) + } + }) + + // Unsupported type kinds should return an error and not create anything + t.Run("create_unsupported_type", func(t *testing.T) { + err := m.CreateType(unsupportedTypeName, "Unsupported", "Unsupported") + if err == nil { + t.Fatalf("Expected error when creating unsupported type %s, got nil", unsupportedTypeName) + } + + // Ensure the type was NOT created + if m.HasType(unsupportedTypeName) { + t.Fatalf("Type %s should not exist after failed creation", unsupportedTypeName) + } + + // Also ensure DropType is safe to call (idempotent) + if err := m.DropType(unsupportedTypeName); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "does not exist") { + t.Fatalf("Unexpected error dropping type %s: %v", unsupportedTypeName, err) + } + } + + if m.HasType(unsupportedTypeName) { + t.Fatalf("Expected type %s to be absent after drop", unsupportedTypeName) + } + }) + + // Drop tables and types + t.Run("drop_tables_and_types", func(t *testing.T) { + if err := m.DropTable(objectTableName); err != nil { + t.Fatalf("Failed to drop table %s: %v", objectTableName, err) + } + if err := m.DropTable(tableName); err != nil { + t.Fatalf("Failed to drop table %s: %v", tableName, err) + } + + // Drop types + if err := m.DropType(objectTypeName); err != nil { + t.Fatalf("Failed to drop type %s: %v", objectTypeName, err) + } + if err := m.DropType(typeName); err != nil { + t.Fatalf("Failed to drop type %s: %v", typeName, err) + } + + // Verify types are gone via HasType + if m.HasType(typeName) { + t.Fatalf("Expected Oracle type %s to be dropped", typeName) + } + if m.HasType(objectTypeName) { + t.Fatalf("Expected Oracle type %s to be dropped", objectTypeName) + } + }) +} + func TestOracleIndexes(t *testing.T) { if DB.Dialector.Name() != "oracle" { return