mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(oracle): fall back to placeholder when sde extproc fails
Closes #7952
This commit is contained in:
@@ -4124,7 +4124,20 @@ func (s *server) queryRowsWithOracleValueRewriteIfNeeded(sqlText string, timeout
|
||||
if errors.As(err, &panicErr) {
|
||||
rewritten, rewriteErr := rewriteOracleSelectSQL(sqlText, s.loadOracleColumnMeta, false)
|
||||
if rewriteErr == nil && rewritten != sqlText {
|
||||
return s.queryRowsWithTimeout(rewritten, nil, timeoutSecs)
|
||||
rewrittenRows, rewrittenErr := s.queryRowsWithTimeout(rewritten, nil, timeoutSecs)
|
||||
if rewrittenErr == nil {
|
||||
return rewrittenRows, nil
|
||||
}
|
||||
// When the server's ArcSDE extproc agent is broken, the
|
||||
// SDE.ST_AsText rewrite itself fails with ORA-28595 and would
|
||||
// hide the remaining columns; retry once with the placeholder
|
||||
// projection so the query stays readable.
|
||||
if placeholder, ok := oracleExtprocFallbackSQL(sqlText, s.loadOracleColumnMeta, rewrittenErr); ok {
|
||||
if placeholderRows, placeholderErr := s.queryRowsWithTimeout(placeholder, nil, timeoutSecs); placeholderErr == nil {
|
||||
return placeholderRows, nil
|
||||
}
|
||||
}
|
||||
return nil, rewrittenErr
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
@@ -4156,6 +4169,34 @@ func oracleColumnTypeNamesContainXMLType(typeNames []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isOracleExtprocAgentFailure reports whether err is an Oracle extproc agent
|
||||
// failure (ORA-28595, typically raised through SDE.ST_GEOMETRY_SHAPELIB_PKG or
|
||||
// SDE.ST_GEOMETRY_OPERATORS) that makes every SDE.ST_GEOMETRY function call
|
||||
// unusable on the server. Matching stays deliberately narrow: the ORA code
|
||||
// must be present; SDE package names alone never trigger it.
|
||||
func isOracleExtprocAgentFailure(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
// Accept both the "ORA-28595" form and a bare "28595" code.
|
||||
return strings.Contains(strings.ToUpper(err.Error()), "28595")
|
||||
}
|
||||
|
||||
// oracleExtprocFallbackSQL returns the placeholder projection to retry with
|
||||
// when the panic-fallback SDE.ST_AsText rewrite failed because of a broken
|
||||
// extproc agent (ORA-28595). It reports false when the error is unrelated or
|
||||
// no placeholder rewrite applies.
|
||||
func oracleExtprocFallbackSQL(sqlText string, loadColumns oracleColumnMetaLoader, rewriteErr error) (string, bool) {
|
||||
if !isOracleExtprocAgentFailure(rewriteErr) {
|
||||
return "", false
|
||||
}
|
||||
placeholder, err := rewriteOracleSelectSQL(sqlText, loadColumns, true)
|
||||
if err != nil || placeholder == sqlText {
|
||||
return "", false
|
||||
}
|
||||
return placeholder, true
|
||||
}
|
||||
|
||||
func (s *server) rewriteXMLTypeSelectSQL(sqlText string) (string, error) {
|
||||
return rewriteOracleXMLTypeSelectSQL(sqlText, s.loadOracleColumnMeta)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/url"
|
||||
@@ -2361,6 +2362,126 @@ func TestRewriteOracleSTGeometrySkipsSetQueries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsOracleExtprocAgentFailure(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "ORA-28595 with SDE package context",
|
||||
err: errors.New(`ORA-28595: Extproc agent: Invalid DLL Path SDE.ST_GEOMETRY_SHAPELIB_PKG`),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ORA-28595 with operator package context",
|
||||
err: errors.New(`ORA-28595: Extproc agent: Invalid DLL Path SDE.ST_GEOMETRY_OPERATORS`),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "lowercase ORA-28595",
|
||||
err: errors.New("ora-28595: extproc agent: invalid dll path"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "bare ORA code",
|
||||
err: errors.New("28595: Extproc agent: Invalid DLL Path"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped ORA-28595",
|
||||
err: fmt.Errorf("query failed: %w", errors.New("ORA-28595: Extproc agent: Invalid DLL Path")),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unrelated ORA error",
|
||||
err: errors.New("ORA-00942: table or view does not exist"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "connection error",
|
||||
err: errors.New("ORA-12170: TNS:Connect timeout occurred"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "SDE package without ORA-28595",
|
||||
err: errors.New("SDE.ST_GEOMETRY_SHAPELIB_PKG raised an error"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := isOracleExtprocAgentFailure(tc.err); got != tc.want {
|
||||
t.Fatalf("isOracleExtprocAgentFailure(%v) = %v, want %v", tc.err, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleExtprocFallbackSQL(t *testing.T) {
|
||||
sdeColumns := func(schema, table string) ([]oracleColumnMeta, error) {
|
||||
return []oracleColumnMeta{
|
||||
{Name: "ID", DataType: "NUMBER"},
|
||||
{Name: "GEOM", DataType: "ST_GEOMETRY", DataTypeOwner: "SDE"},
|
||||
}, nil
|
||||
}
|
||||
t.Run("returns placeholder projection for extproc failure", func(t *testing.T) {
|
||||
sqlText, ok := oracleExtprocFallbackSQL(
|
||||
`SELECT * FROM SDE.TEST_GEOM`,
|
||||
sdeColumns,
|
||||
errors.New("ORA-28595: Extproc agent: Invalid DLL Path"),
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("expected placeholder fallback SQL")
|
||||
}
|
||||
if !strings.Contains(sqlText, "'<ST_GEOMETRY>'") {
|
||||
t.Fatalf("expected placeholder expression, got %s", sqlText)
|
||||
}
|
||||
if strings.Contains(sqlText, "SDE.ST_AsText") {
|
||||
t.Fatalf("placeholder projection must not call SDE.ST_AsText, got %s", sqlText)
|
||||
}
|
||||
})
|
||||
t.Run("keeps ST_AsText error for unrelated failures", func(t *testing.T) {
|
||||
sqlText, ok := oracleExtprocFallbackSQL(
|
||||
`SELECT * FROM SDE.TEST_GEOM`,
|
||||
sdeColumns,
|
||||
errors.New("ORA-12170: TNS:Connect timeout occurred"),
|
||||
)
|
||||
if ok {
|
||||
t.Fatalf("unexpected fallback SQL: %s", sqlText)
|
||||
}
|
||||
})
|
||||
t.Run("no fallback without rewriteable columns", func(t *testing.T) {
|
||||
sqlText, ok := oracleExtprocFallbackSQL(
|
||||
`SELECT * FROM SDE.TEST_GEOM`,
|
||||
func(schema, table string) ([]oracleColumnMeta, error) {
|
||||
return []oracleColumnMeta{{Name: "ID", DataType: "NUMBER"}}, nil
|
||||
},
|
||||
errors.New("ORA-28595: Extproc agent: Invalid DLL Path"),
|
||||
)
|
||||
if ok {
|
||||
t.Fatalf("unexpected fallback SQL: %s", sqlText)
|
||||
}
|
||||
})
|
||||
t.Run("no fallback when column metadata fails", func(t *testing.T) {
|
||||
sqlText, ok := oracleExtprocFallbackSQL(
|
||||
`SELECT * FROM SDE.TEST_GEOM`,
|
||||
func(schema, table string) ([]oracleColumnMeta, error) {
|
||||
return nil, errors.New("ORA-00942: table or view does not exist")
|
||||
},
|
||||
errors.New("ORA-28595: Extproc agent: Invalid DLL Path"),
|
||||
)
|
||||
if ok {
|
||||
t.Fatalf("unexpected fallback SQL: %s", sqlText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRewriteOracleXMLTypeSkipsJoins(t *testing.T) {
|
||||
called := false
|
||||
sqlText, err := rewriteOracleXMLTypeSelectSQL(
|
||||
@@ -2856,6 +2977,7 @@ type oracleViewSourceQueryStep struct {
|
||||
rows [][]driver.Value
|
||||
err error
|
||||
exec bool
|
||||
panicText string
|
||||
}
|
||||
|
||||
type oracleViewSourceDriver struct {
|
||||
@@ -2899,6 +3021,9 @@ func (c *oracleViewSourceConn) QueryContext(
|
||||
if !strings.Contains(query, step.queryContains) {
|
||||
return nil, errors.New("unexpected query: " + query)
|
||||
}
|
||||
if step.panicText != "" {
|
||||
panic(step.panicText)
|
||||
}
|
||||
values := make([]driver.Value, len(args))
|
||||
for index, arg := range args {
|
||||
values[index] = arg.Value
|
||||
@@ -3172,6 +3297,83 @@ func TestOracleQueryRowsRecoversDriverPanic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleQueryRowsFallsBackToPlaceholderOnExtprocFailure(t *testing.T) {
|
||||
metadataStep := oracleViewSourceQueryStep{
|
||||
queryContains: "ALL_TAB_COLUMNS",
|
||||
args: []driver.Value{"SDE", "TEST_GEOM"},
|
||||
columns: []string{"COLUMN_NAME", "DATA_TYPE", "DATA_TYPE_OWNER"},
|
||||
rows: [][]driver.Value{
|
||||
{"ID", "NUMBER", nil},
|
||||
{"GEOM", "ST_GEOMETRY", "SDE"},
|
||||
},
|
||||
}
|
||||
db, _ := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{
|
||||
{queryContains: "SELECT * FROM SDE.TEST_GEOM", args: []driver.Value{}, panicText: "simulated go-ora panic on SDE object column"},
|
||||
metadataStep,
|
||||
{queryContains: "SDE.ST_AsText", args: []driver.Value{}, err: errors.New("ORA-28595: Extproc agent: Invalid DLL Path")},
|
||||
metadataStep,
|
||||
{
|
||||
queryContains: "'<ST_GEOMETRY>'",
|
||||
args: []driver.Value{},
|
||||
columns: []string{"ID", "GEOM", "__DBX_LARGE_VALUE_BYTES_C_1"},
|
||||
rows: [][]driver.Value{{int64(1), "<ST_GEOMETRY>", "D:1"}},
|
||||
},
|
||||
})
|
||||
s := newServer()
|
||||
s.db = db
|
||||
rows, err := s.queryRowsWithOracleValueRewriteIfNeeded("SELECT * FROM SDE.TEST_GEOM", 0, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected placeholder fallback to serve rows, got %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(columns, []string{"ID", "GEOM", "__DBX_LARGE_VALUE_BYTES_C_1"}) {
|
||||
t.Fatalf("unexpected placeholder columns: %v", columns)
|
||||
}
|
||||
if !rows.Next() {
|
||||
t.Fatalf("expected one placeholder row, got %v", rows.Err())
|
||||
}
|
||||
var id int64
|
||||
var geom, marker string
|
||||
if err := rows.Scan(&id, &geom, &marker); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != 1 || geom != "<ST_GEOMETRY>" || marker != "D:1" {
|
||||
t.Fatalf("unexpected placeholder row: %v %v %v", id, geom, marker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleQueryRowsKeepsSTAsTextErrorWhenPlaceholderAlsoFails(t *testing.T) {
|
||||
metadataStep := oracleViewSourceQueryStep{
|
||||
queryContains: "ALL_TAB_COLUMNS",
|
||||
args: []driver.Value{"SDE", "TEST_GEOM"},
|
||||
columns: []string{"COLUMN_NAME", "DATA_TYPE", "DATA_TYPE_OWNER"},
|
||||
rows: [][]driver.Value{
|
||||
{"ID", "NUMBER", nil},
|
||||
{"GEOM", "ST_GEOMETRY", "SDE"},
|
||||
},
|
||||
}
|
||||
db, _ := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{
|
||||
{queryContains: "SELECT * FROM SDE.TEST_GEOM", args: []driver.Value{}, panicText: "simulated go-ora panic on SDE object column"},
|
||||
metadataStep,
|
||||
{queryContains: "SDE.ST_AsText", args: []driver.Value{}, err: errors.New("ORA-28595: Extproc agent: Invalid DLL Path")},
|
||||
metadataStep,
|
||||
{queryContains: "'<ST_GEOMETRY>'", args: []driver.Value{}, err: errors.New("ORA-01013: user requested cancel of current operation")},
|
||||
})
|
||||
s := newServer()
|
||||
s.db = db
|
||||
rows, err := s.queryRowsWithOracleValueRewriteIfNeeded("SELECT * FROM SDE.TEST_GEOM", 0, false)
|
||||
if rows != nil {
|
||||
t.Fatal("failed fallback path must not return rows")
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), "ORA-28595") {
|
||||
t.Fatalf("expected the original ST_AsText ORA-28595 error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualTransactionBeginCommitRollback(t *testing.T) {
|
||||
db, driver := openOracleManualTxTestDB(t)
|
||||
s := newServer()
|
||||
|
||||
Reference in New Issue
Block a user