diff --git a/.gitea/workflows/test-oracle.yml b/.gitea/workflows/test-oracle.yml new file mode 100644 index 00000000..5963a15b --- /dev/null +++ b/.gitea/workflows/test-oracle.yml @@ -0,0 +1,38 @@ +name: test oracle +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + name: test oracle + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: test oracle + env: + TEST_ORACLE_HOST: oracle:1521 + TEST_ORACLE_DBNAME: FREEPDB1 + TEST_ORACLE_USERNAME: system + TEST_ORACLE_PASSWORD: oracle + run: TEST_CACHE_ENABLE=false make test-oracle + + services: + oracle: + image: gvenzl/oracle-xe:latest + env: + ORACLE_RANDOM_PASSWORD: true + APP_USER: system + APP_USER_PASSWORD: oracle + ports: + - 1521:1521 + options: >- + --health-cmd healthcheck.sh + --health-interval 10s + --health-timeout 5s + --health-retries 10 \ No newline at end of file diff --git a/Makefile b/Makefile index 5dc67e0f..8651f058 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,12 @@ TEST_DAMENG_HOST ?= dameng:5236 TEST_DAMENG_USERNAME ?= SYSDBA TEST_DAMENG_PASSWORD ?= SYSDBA +TEST_ORACLE_HOST ?= oracle:1521 +TEST_ORACLE_SCHEMA ?= +TEST_ORACLE_DBNAME ?= xe +TEST_ORACLE_USERNAME ?= system +TEST_ORACLE_PASSWORD ?= oracle + TEST_CACHE_ENABLE ?= false TEST_QUOTE_POLICY ?= always @@ -279,6 +285,21 @@ test-dameng\#%: go-check -conn_str="dm://$(TEST_DAMENG_USERNAME):$(TEST_DAMENG_PASSWORD)@$(TEST_DAMENG_HOST)" \ -coverprofile=dameng.$(TEST_QUOTE_POLICY).$(TEST_CACHE_ENABLE).coverage.out -covermode=atomic -timeout=20m +.PHONY: test-oracle +test-oracle: test-goora + +.PNONY: test-goora +test-goora: go-check + $(GO) test $(INTEGRATION_PACKAGES) -v -race -db=goora -schema='$(TEST_ORACLE_SCHEMA)' -cache=$(TEST_CACHE_ENABLE) \ + -conn_str="$(TEST_ORACLE_USERNAME):$(TEST_ORACLE_PASSWORD)@$(TEST_ORACLE_HOST)/$(TEST_ORACLE_DBNAME)" \ + -coverprofile=oracle.$(TEST_CACHE_ENABLE).coverage.out -covermode=atomic + +.PHONY: test-oci8\#% +test-goora\#%: go-check + $(GO) test $(INTEGRATION_PACKAGES) -v -race -run $* -db=goora -schema='$(TEST_PGSQL_SCHEMA)' -cache=$(TEST_CACHE_ENABLE) \ + -conn_str="oracle://$(TEST_ORACLE_USERNAME):$(TEST_ORACLE_PASSWORD)@$(TEST_ORACLE_HOST)/$(TEST_ORACLE_DBNAME)" \ + -coverprofile=oracle.$(TEST_CACHE_ENABLE).coverage.out -covermode=atomic + .PHONY: vet vet: $(GO) vet $(shell $(GO) list ./...) diff --git a/dialects/oracle.go b/dialects/oracle.go index 5f614b1a..6caea1a0 100644 --- a/dialects/oracle.go +++ b/dialects/oracle.go @@ -14,6 +14,7 @@ import ( "strings" "xorm.io/xorm/core" + "xorm.io/xorm/internal/utils" "xorm.io/xorm/schemas" ) @@ -567,11 +568,13 @@ func (db *oracle) SQLType(c *schemas.Column) string { c.Default = "0" } res = "NUMBER(1,0)" - case schemas.Bit, schemas.TinyInt, schemas.SmallInt, schemas.MediumInt, schemas.Int, schemas.Integer, schemas.BigInt, schemas.Serial, schemas.BigSerial: + case schemas.Bit, schemas.TinyInt, schemas.SmallInt, schemas.MediumInt, schemas.Int, + schemas.Integer, schemas.BigInt, schemas.Serial, schemas.BigSerial, + schemas.UnsignedBigInt, schemas.UnsignedBit, schemas.UnsignedInt: res = "NUMBER" case schemas.Binary, schemas.VarBinary, schemas.Blob, schemas.TinyBlob, schemas.MediumBlob, schemas.LongBlob, schemas.Bytea: return schemas.Blob - case schemas.Time, schemas.DateTime, schemas.TimeStamp: + case schemas.Date, schemas.Time, schemas.DateTime, schemas.TimeStamp: res = schemas.TimeStamp case schemas.TimeStampz: res = "TIMESTAMP WITH TIME ZONE" @@ -695,9 +698,29 @@ func (db *oracle) IsColumnExist(queryer core.Queryer, ctx context.Context, table return db.HasRecords(queryer, ctx, query, args...) } +func QueryRowContext(ctx context.Context, queryer core.Queryer, query string, args ...interface{}) *core.Row { + rows, err := queryer.QueryContext(ctx, query, args...) + if err != nil { + return core.NewRow(nil, err) + } + return core.NewRow(rows, nil) +} + func (db *oracle) GetColumns(queryer core.Queryer, ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) { + s := `select column_name from user_cons_columns + where constraint_name = (select constraint_name from user_constraints + where table_name = :1 and constraint_type ='P')` + var pkName string + err := QueryRowContext(ctx, queryer, s, tableName).Scan(&pkName) + if err != nil { + if err == sql.ErrNoRows { + err = nil + } + return nil, nil, err + } + args := []interface{}{tableName} - s := "SELECT column_name,data_default,data_type,data_length,data_precision,data_scale," + + s = "SELECT column_name,data_default,data_type,data_length,data_precision,data_scale," + "nullable FROM USER_TAB_COLUMNS WHERE table_name = :1" rows, err := queryer.QueryContext(ctx, s, args...) @@ -712,11 +735,11 @@ func (db *oracle) GetColumns(queryer core.Queryer, ctx context.Context, tableNam col := new(schemas.Column) col.Indexes = make(map[string]int) - var colName, colDefault, nullable, dataType, dataPrecision, dataScale *string + var colName, colDefault, nullable, dataType, dataPrecision, dataScale, comment *string var dataLen int64 err = rows.Scan(&colName, &colDefault, &dataType, &dataLen, &dataPrecision, - &dataScale, &nullable) + &dataScale, &nullable, &comment) if err != nil { return nil, nil, err } @@ -733,6 +756,21 @@ func (db *oracle) GetColumns(queryer core.Queryer, ctx context.Context, tableNam col.Nullable = false } + if comment != nil { + col.Comment = *comment + } + + if pkName != "" && pkName == col.Name { + col.IsPrimaryKey = true + has, err := db.HasRecords(queryer, ctx, "SELECT * FROM USER_SEQUENCES WHERE SEQUENCE_NAME = :1", utils.SeqName(tableName)) + if err != nil { + return nil, nil, err + } + if has { + col.IsAutoIncrement = true + } + } + var ignore bool var dt string @@ -758,7 +796,7 @@ func (db *oracle) GetColumns(queryer core.Queryer, ctx context.Context, tableNam col.SQLType = schemas.SQLType{Name: schemas.TimeStampz, DefaultLength: 0, DefaultLength2: 0} case "NUMBER": col.SQLType = schemas.SQLType{Name: schemas.Double, DefaultLength: len1, DefaultLength2: len2} - case "LONG", "LONG RAW": + case "LONG", "LONG RAW", "NCLOB", "CLOB": col.SQLType = schemas.SQLType{Name: schemas.Text, DefaultLength: 0, DefaultLength2: 0} case "RAW": col.SQLType = schemas.SQLType{Name: schemas.Binary, DefaultLength: 0, DefaultLength2: 0} diff --git a/tests/tests.go b/tests/tests.go index dfc04d09..31c781e7 100644 --- a/tests/tests.go +++ b/tests/tests.go @@ -111,6 +111,35 @@ func createEngine(dbType, connStr string) error { } connStr = u.Path *ignoreSelectUpdate = true + case schemas.ORACLE, "goora": + db, err := sql.Open("oracle", connStr) + if err != nil { + return err + } + rows, err := db.Query("SELECT 1 FROM pg_database WHERE datname = 'xorm_test'") + if err != nil { + return fmt.Errorf("db.Query: %v", err) + } + defer rows.Close() + + if !rows.Next() { + if _, err = db.Exec("CREATE DATABASE xorm_test"); err != nil { + return fmt.Errorf("CREATE DATABASE: %v", err) + } + } + if *schema != "" { + db.Close() + db, err = sql.Open(dbType, connStr) + if err != nil { + return err + } + defer db.Close() + if _, err = db.Exec("CREATE SCHEMA IF NOT EXISTS " + *schema); err != nil { + return fmt.Errorf("CREATE SCHEMA: %v", err) + } + } + db.Close() + *ignoreSelectUpdate = true default: *ignoreSelectUpdate = true }