Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions oracle/migrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
169 changes: 169 additions & 0 deletions tests/migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading