mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(oracle): render placeholder for user-defined type columns
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user