fix(oracle): render placeholder for user-defined type columns

This commit is contained in:
zipg
2026-09-24 23:48:53 +08:00
committed by GitHub
parent 3640f6827e
commit ca9bfae5b9
2 changed files with 219 additions and 3 deletions
+88 -3
View File
@@ -3866,7 +3866,7 @@ func (s *server) runPagedOracleSelect(sqlText string, opts queryOptions, pageSiz
if err != nil {
s.closeRows(rows)
var panicErr oracleDriverPanicError
if attempt == 0 && errors.As(err, &panicErr) {
if attempt == 0 && !oracleSQLLocksRows(sqlText) && errors.As(err, &panicErr) {
if placeholder, ok := oraclePlaceholderRetrySQL(sqlText, s.loadOracleColumnMeta); ok {
sqlText = placeholder
continue
@@ -4211,10 +4211,32 @@ func columnTypeNames(rows *sql.Rows) []string {
}
func (s *server) queryRowsWithOracleValueRewriteIfNeeded(sqlText string, timeoutSecs int, deferLOBs bool) (*sql.Rows, error) {
// A statement that keeps row locks while it runs is rewritten before the
// first attempt: when such a statement panics after the rows were locked,
// the locks stay behind and the retry below blocks on them until the query
// times out (issues #9344 / #9180).
if oracleSQLLocksRows(sqlText) {
rewritten, err := rewriteOracleSelectSQL(sqlText, s.loadOracleColumnMeta, deferLOBs)
if err == nil && rewritten != sqlText {
return s.queryRowsWithTimeout(rewritten, nil, timeoutSecs)
}
return s.queryRowsWithTimeout(sqlText, nil, timeoutSecs)
}
if deferLOBs {
rewritten, err := rewriteOracleSelectSQL(sqlText, s.loadOracleColumnMeta, true)
if err == nil && rewritten != sqlText {
return s.queryRowsWithTimeout(rewritten, nil, timeoutSecs)
rows, rewrittenErr := s.queryRowsWithTimeout(rewritten, nil, timeoutSecs)
if rewrittenErr == nil {
return rows, nil
}
// A deferred projection can still read a value the driver cannot
// decode (user-defined object/collection columns). Only a panic is
// worth retrying through the value-rewrite fallback below; other
// failures keep their own error.
var panicErr oracleDriverPanicError
if !errors.As(rewrittenErr, &panicErr) {
return nil, rewrittenErr
}
}
}
rows, err := s.queryRowsWithTimeout(sqlText, nil, timeoutSecs)
@@ -4589,6 +4611,8 @@ func rewriteOracleSelectItems(items []string, columns []oracleColumnMeta, tableR
rewritten = append(rewritten, oracleGeometryExpression(column, columnRef, outputAlias))
} else if isOracleXMLType(column.DataType) && !deferLOBs {
rewritten = append(rewritten, oracleXMLSerializeExpression(columnRef, outputAlias))
} else if isOracleOpaqueObjectType(column) {
rewritten = append(rewritten, oracleOpaqueObjectExpression(column, columnRef, outputAlias))
} else if deferLOBs {
if expressions, ok := oracleDeferredLOBExpressions(columnRef, outputAlias, sourceIndex, column); ok {
rewritten = append(rewritten, expressions...)
@@ -4623,6 +4647,12 @@ func rewriteOracleSelectItems(items []string, columns []oracleColumnMeta, tableR
sourceIndex++
continue
}
if isOracleOpaqueObjectType(meta) {
rewritten = append(rewritten, oracleOpaqueObjectExpression(meta, columnRef, outputAlias))
changed = true
sourceIndex++
continue
}
if deferLOBs {
if expressions, isLOB := oracleDeferredLOBExpressions(columnRef, outputAlias, sourceIndex, meta); isLOB {
rewritten = append(rewritten, expressions...)
@@ -4676,6 +4706,41 @@ func oracleGeometryPlaceholder(column oracleColumnMeta) string {
return "<ST_GEOMETRY>"
}
// oracleOpaqueObjectExpression projects a placeholder instead of the value of a
// user-defined type (object, collection, ANYDATA, ...). go-ora cannot decode
// those values over the wire and panics while scanning them, which fails the
// whole statement and rolls the transaction back (issues #9344 / #9180). The
// placeholder keeps the statement and the remaining columns readable.
func oracleOpaqueObjectExpression(column oracleColumnMeta, columnRef, alias string) string {
return fmt.Sprintf("CASE WHEN %s IS NULL THEN NULL ELSE '%s' END AS %s", columnRef, oracleOpaqueObjectPlaceholder(column), alias)
}
func oracleOpaqueObjectPlaceholder(column oracleColumnMeta) string {
name := strings.ToUpper(strings.TrimSpace(column.DataType))
if name == "" {
name = "OBJECT"
}
return "<" + name + ">"
}
// oracleSQLLocksRows reports whether a statement holds row locks while it runs
// (FOR UPDATE, FOR UPDATE OF ..., NOWAIT / WAIT n / SKIP LOCKED). Such a
// statement must not be retried after a driver panic: the failed attempt keeps
// its locks, so the retry would block on them instead of returning.
func oracleSQLLocksRows(sqlText string) bool {
for searchFrom := 0; searchFrom < len(sqlText); {
pos := findTopLevelSQLKeyword(sqlText, searchFrom, "for")
if pos < 0 {
return false
}
if sqlKeywordAt(sqlText, skipSQLWhitespace(sqlText, pos+len("for")), "update") {
return true
}
searchFrom = pos + len("for")
}
return false
}
func oracleDeferredLOBKind(dataType string) (kind, placeholder string, ok bool) {
switch strings.ToUpper(strings.TrimSpace(dataType)) {
case "CLOB":
@@ -4773,7 +4838,15 @@ func oracleQualifierMatchesTable(qualifier string, tableRef oracleTableRef) bool
func oracleColumnsNeedValueRewrite(columns []oracleColumnMeta, deferLOBs bool) bool {
for _, column := range columns {
if isOracleGeometryType(column) || isOracleXMLType(column.DataType) || (deferLOBs && isOracleDeferredLOBType(column.DataType)) {
if isOracleGeometryType(column) || isOracleXMLType(column.DataType) {
return true
}
if deferLOBs && isOracleDeferredLOBType(column.DataType) {
return true
}
// Opaque user-defined types are substituted in both the plain and the
// deferred preview projection, so a preview never panics on them.
if isOracleOpaqueObjectType(column) {
return true
}
}
@@ -4822,6 +4895,18 @@ func isOracleGeometryType(column oracleColumnMeta) bool {
return isOracleSTGeometry(column) || isOracleSDOGeometry(column)
}
// isOracleOpaqueObjectType reports whether a column's declared type is a named
// user-defined type that the driver cannot decode. Built-in scalar types report
// no DATA_TYPE_OWNER; named types (object, collection, ANYDATA, ...) do. Types
// DBX is able to render — SDE/SDO geometry, XMLType and deferred LOBs — are
// matched earlier, so they are never treated as opaque here.
func isOracleOpaqueObjectType(column oracleColumnMeta) bool {
if isOracleGeometryType(column) || isOracleXMLType(column.DataType) || isOracleDeferredLOBType(column.DataType) {
return false
}
return strings.TrimSpace(column.DataTypeOwner) != ""
}
func leadingSQLSelectListStart(sqlText string) int {
trimmed := trimLeadingSQLComments(sqlText)
prefixLen := len(sqlText) - len(trimmed)
+131
View File
@@ -4095,3 +4095,134 @@ func (r *oracleManualTxRows) Next(dest []driver.Value) error {
r.next++
return nil
}
func TestOracleOpaqueObjectTypeDetection(t *testing.T) {
cases := []struct {
name string
column oracleColumnMeta
want bool
}{
{"object type", oracleColumnMeta{Name: "G", DataType: "GEOM_T", DataTypeOwner: "DBX_TEST"}, true},
{"collection type", oracleColumnMeta{Name: "V", DataType: "DBX9180_VARRAY", DataTypeOwner: "DBX_TEST"}, true},
{"anydata", oracleColumnMeta{Name: "V", DataType: "ANYDATA", DataTypeOwner: "SYS"}, true},
{"number", oracleColumnMeta{Name: "ID", DataType: "NUMBER"}, false},
{"varchar2", oracleColumnMeta{Name: "NOTE", DataType: "VARCHAR2"}, false},
{"clob", oracleColumnMeta{Name: "PAYLOAD", DataType: "CLOB"}, false},
{"xmltype", oracleColumnMeta{Name: "DOC", DataType: "XMLTYPE", DataTypeOwner: "SYS"}, false},
{"sdo geometry", oracleColumnMeta{Name: "SHAPE", DataType: "SDO_GEOMETRY", DataTypeOwner: "MDSYS"}, false},
{"sde geometry", oracleColumnMeta{Name: "SHAPE", DataType: "ST_GEOMETRY", DataTypeOwner: "SDE"}, false},
}
for _, testCase := range cases {
if got := isOracleOpaqueObjectType(testCase.column); got != testCase.want {
t.Fatalf("%s: isOracleOpaqueObjectType() = %v, want %v", testCase.name, got, testCase.want)
}
}
}
func TestRewriteOracleOpaqueObjectSelectStar(t *testing.T) {
sqlText, err := rewriteOracleSelectSQL(
`SELECT * FROM TEST_UDT`,
func(schema, table string) ([]oracleColumnMeta, error) {
return []oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "G", DataType: "GEOM_T", DataTypeOwner: "DBX_TEST"},
{Name: "NOTE", DataType: "VARCHAR2"},
}, nil
},
false,
)
if err != nil {
t.Fatal(err)
}
want := `SELECT "ID", CASE WHEN "G" IS NULL THEN NULL ELSE '<GEOM_T>' END AS "G", "NOTE" FROM TEST_UDT`
if sqlText != want {
t.Fatalf("rewriteOracleSelectSQL() = %s, want %s", sqlText, want)
}
}
func TestRewriteOracleOpaqueObjectExplicitColumn(t *testing.T) {
sqlText, err := rewriteOracleSelectSQL(
`SELECT t.G AS shape, t.ID FROM TEST_UDT t`,
func(schema, table string) ([]oracleColumnMeta, error) {
return []oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "G", DataType: "GEOM_T", DataTypeOwner: "DBX_TEST"},
}, nil
},
false,
)
if err != nil {
t.Fatal(err)
}
want := `SELECT CASE WHEN t."G" IS NULL THEN NULL ELSE '<GEOM_T>' END AS shape, t.ID FROM TEST_UDT t`
if sqlText != want {
t.Fatalf("rewriteOracleSelectSQL() = %s, want %s", sqlText, want)
}
}
func TestRewriteOracleKeepsBuiltinColumnsUntouched(t *testing.T) {
input := `SELECT ID, NOTE FROM TEST_PLAIN`
sqlText, err := rewriteOracleSelectSQL(
input,
func(schema, table string) ([]oracleColumnMeta, error) {
return []oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "NOTE", DataType: "VARCHAR2"},
{Name: "PAYLOAD", DataType: "CLOB"},
}, nil
},
false,
)
if err != nil {
t.Fatal(err)
}
if sqlText != input {
t.Fatalf("rewriteOracleSelectSQL() = %s, want %s", sqlText, input)
}
}
func TestOracleSQLLocksRowsDetection(t *testing.T) {
cases := []struct {
name string
sql string
want bool
}{
{"for update", "SELECT * FROM T FOR UPDATE", true},
{"for update skip locked", "SELECT * FROM T FOR UPDATE SKIP LOCKED", true},
{"for update of column", "SELECT ID FROM T FOR UPDATE OF ID NOWAIT", true},
{"lowercase", "select id from t for update", true},
{"plain select", "SELECT * FROM T", false},
{"string literal", "SELECT 'FOR UPDATE' FROM T", false},
{"comment", "SELECT * FROM T -- FOR UPDATE", false},
{"subquery for update", "SELECT * FROM (SELECT ID FROM T FOR UPDATE) X", false},
{"column named update", "SELECT FOR_UPDATE FROM T", false},
}
for _, testCase := range cases {
if got := oracleSQLLocksRows(testCase.sql); got != testCase.want {
t.Fatalf("%s: oracleSQLLocksRows(%q) = %v, want %v", testCase.name, testCase.sql, got, testCase.want)
}
}
}
func TestRewriteOracleOpaqueObjectInDeferredProjection(t *testing.T) {
sqlText, err := rewriteOracleSelectSQL(
`SELECT * FROM TEST_UDT_LOB`,
func(schema, table string) ([]oracleColumnMeta, error) {
return []oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "DOC", DataType: "CLOB"},
{Name: "G", DataType: "GEOM_T", DataTypeOwner: "DBX_TEST"},
}, nil
},
true,
)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(sqlText, `CASE WHEN "G" IS NULL THEN NULL ELSE '<GEOM_T>' END AS "G"`) {
t.Fatalf("deferred rewrite did not substitute the opaque column: %s", sqlText)
}
if !strings.Contains(sqlText, `"DOC"`) {
t.Fatalf("deferred rewrite dropped the LOB column: %s", sqlText)
}
}