From 295db2903b7b519ea0f0c8e60f0a2880751e53f3 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 5 Sep 2026 13:25:41 +0800 Subject: [PATCH] fix(oracle): fall back to placeholder when sde extproc fails Closes #7952 --- agents/drivers/oracle-go/main.go | 43 +++++- agents/drivers/oracle-go/main_test.go | 202 ++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go index 425802e3ea..00daec6168 100644 --- a/agents/drivers/oracle-go/main.go +++ b/agents/drivers/oracle-go/main.go @@ -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) } diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go index 40d74cdb14..ac6ed234e9 100644 --- a/agents/drivers/oracle-go/main_test.go +++ b/agents/drivers/oracle-go/main_test.go @@ -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, "''") { + 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: "''", + args: []driver.Value{}, + columns: []string{"ID", "GEOM", "__DBX_LARGE_VALUE_BYTES_C_1"}, + rows: [][]driver.Value{{int64(1), "", "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 != "" || 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: "''", 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()