fix(oracle): defer legacy LOB connection switch

Closes #5242
This commit is contained in:
t8y2 2026-08-04 01:35:59 +08:00
parent 44ef416d2f
commit 0ece86c478
No known key found for this signature in database
2 changed files with 140 additions and 35 deletions

View File

@ -31,7 +31,7 @@ const defaultMaxRows = 1000
const oracleCharsetZHS32GB18030 = 854
const legacyAgentSessionID = "__legacy__"
const maxAgentSessions = 256
const oracleLegacyLOBMaxMajorVersion = 11
const oracleLegacyLOBMaxMajorVersion = 10
const oracleDatabaseVersionProbeTimeout = 3 * time.Second
const oracleDatabaseVersionSQL = `
@ -390,6 +390,7 @@ type triggerInfo struct {
type server struct {
db *sql.DB
params connectParams
legacyLOBFetchDeferred bool
sessions map[string]*querySession
tableReadSessions map[string]*querySession
nextSessionID int64
@ -669,6 +670,11 @@ func (s *server) handleLine(line string) (response, bool) {
}
func (s *server) dispatch(method string, params map[string]json.RawMessage) (any, bool, error) {
if oracleMethodMayReadLOB(method) {
if err := s.ensureLegacyOracleLOBFetch(); err != nil {
return nil, false, err
}
}
switch method {
case "handshake":
return map[string]any{
@ -806,46 +812,29 @@ func (s *server) dispatch(method string, params map[string]json.RawMessage) (any
func (s *server) connect(params connectParams) error {
_ = s.disconnect()
db, effectiveParams, err := openSessionDB(params, 15*time.Second)
db, err := openConfiguredSessionDB(params, 15*time.Second)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if _, err := db.ExecContext(ctx, "ALTER SESSION SET NLS_LANGUAGE='AMERICAN'"); err != nil {
db.Close()
return err
}
majorVersion, versionKnown := oracleServerMajorVersion(db, 15*time.Second)
s.db = db
s.params = effectiveParams
s.params = params
s.legacyLOBFetchDeferred = shouldUseLegacyOracleLOBFetch(params, majorVersion, versionKnown)
return nil
}
func openSessionDB(params connectParams, timeout time.Duration) (*sql.DB, connectParams, error) {
func openConfiguredSessionDB(params connectParams, timeout time.Duration) (*sql.DB, error) {
db, err := openAndPingDB(params, timeout)
if err != nil {
return nil, params, err
return nil, err
}
if hasOracleLOBFetchOption(params) {
return db, params, nil
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if _, err := db.ExecContext(ctx, "ALTER SESSION SET NLS_LANGUAGE='AMERICAN'"); err != nil {
db.Close()
return nil, err
}
majorVersion, ok := oracleServerMajorVersion(db, timeout)
if !shouldUseLegacyOracleLOBFetch(params, majorVersion, ok) {
return db, params, nil
}
effectiveParams := withOracleLOBFetchPost(params)
if effectiveParams == params {
return db, params, nil
}
if err := db.Close(); err != nil {
return nil, params, err
}
db, err = openAndPingDB(effectiveParams, timeout)
if err != nil {
return nil, params, err
}
return db, effectiveParams, nil
return db, nil
}
func oracleServerMajorVersion(db *sql.DB, timeout time.Duration) (int, bool) {
@ -854,6 +843,9 @@ func oracleServerMajorVersion(db *sql.DB, timeout time.Duration) (int, bool) {
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if majorVersion, ok := oracleServerMajorVersionFromDBConn(ctx, db); ok {
return majorVersion, true
}
for _, query := range oracleDatabaseVersionQueries {
var version string
err := db.QueryRowContext(ctx, query).Scan(&version)
@ -866,6 +858,40 @@ func oracleServerMajorVersion(db *sql.DB, timeout time.Duration) (int, bool) {
return 0, false
}
func oracleServerMajorVersionFromDBConn(ctx context.Context, db *sql.DB) (int, bool) {
conn, err := db.Conn(ctx)
if err != nil {
return 0, false
}
defer conn.Close()
var majorVersion int
var versionKnown bool
if err := conn.Raw(func(driverConn any) error {
majorVersion, versionKnown = oracleServerMajorVersionFromDriverConn(driverConn)
return nil
}); err != nil {
return 0, false
}
return majorVersion, versionKnown
}
func oracleServerMajorVersionFromDriverConn(driverConn any) (int, bool) {
conn, ok := driverConn.(*go_ora.Connection)
if !ok {
return 0, false
}
return parseOracleAuthVersionNumber(conn.SessionProperties["AUTH_VERSION_NO"])
}
func parseOracleAuthVersionNumber(value string) (int, bool) {
encoded, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32)
if err != nil {
return 0, false
}
majorVersion := int(encoded >> 24)
return majorVersion, majorVersion > 0
}
func parseOracleMajorVersion(version string) (int, bool) {
match := oracleVersionNumberRegexp.FindStringSubmatch(strings.TrimSpace(version))
if len(match) != 2 {
@ -879,6 +905,38 @@ func shouldUseLegacyOracleLOBFetch(params connectParams, majorVersion int, versi
return versionKnown && majorVersion <= oracleLegacyLOBMaxMajorVersion && !hasOracleLOBFetchOption(params)
}
func oracleMethodMayReadLOB(method string) bool {
switch method {
case "get_table_ddl", "execute_query", "execute_query_page", "start_table_read", "execute_transaction":
return true
default:
return false
}
}
func (s *server) ensureLegacyOracleLOBFetch() error {
if !s.legacyLOBFetchDeferred {
return nil
}
effectiveParams := withOracleLOBFetchPost(s.params)
if effectiveParams == s.params {
s.legacyLOBFetchDeferred = false
return nil
}
db, err := openConfiguredSessionDB(effectiveParams, 15*time.Second)
if err != nil {
return err
}
oldDB := s.db
s.db = db
s.params = effectiveParams
s.legacyLOBFetchDeferred = false
if oldDB != nil {
_ = oldDB.Close()
}
return nil
}
func hasOracleLOBFetchOption(params connectParams) bool {
connectionString := strings.TrimSpace(params.ConnectionString)
if strings.HasPrefix(strings.ToLower(connectionString), "oracle://") {
@ -922,6 +980,7 @@ func withOracleLOBFetchPost(params connectParams) connectParams {
func (s *server) disconnect() error {
s.closeAllQuerySessions()
s.legacyLOBFetchDeferred = false
if s.db == nil {
return nil
}

View File

@ -14,6 +14,7 @@ import (
"testing"
"time"
go_ora "github.com/sijms/go-ora/v2"
"github.com/sijms/go-ora/v2/configurations"
)
@ -919,6 +920,41 @@ func TestParseOracleMajorVersion(t *testing.T) {
}
}
func TestParseOracleAuthVersionNumber(t *testing.T) {
tests := []struct {
value string
major int
ok bool
}{
{value: "169870336", major: 10, ok: true},
{value: "186647040", major: 11, ok: true},
{value: "301989888", major: 18, ok: true},
{value: "", ok: false},
{value: "not-a-version", ok: false},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
major, ok := parseOracleAuthVersionNumber(tt.value)
if major != tt.major || ok != tt.ok {
t.Fatalf("parseOracleAuthVersionNumber(%q) = (%d, %t), want (%d, %t)", tt.value, major, ok, tt.major, tt.ok)
}
})
}
}
func TestOracleServerMajorVersionUsesDriverSessionProperties(t *testing.T) {
major, ok := oracleServerMajorVersionFromDriverConn(&go_ora.Connection{
SessionProperties: map[string]string{"AUTH_VERSION_NO": "186647040"},
})
if !ok || major != 11 {
t.Fatalf("oracleServerMajorVersionFromDriverConn() = (%d, %t), want (11, true)", major, ok)
}
if _, ok := oracleServerMajorVersionFromDriverConn(struct{}{}); ok {
t.Fatal("non-Oracle connections should not expose a server version")
}
}
func TestOracleServerMajorVersionUsesProductComponentVersion(t *testing.T) {
db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{
{
@ -1029,11 +1065,8 @@ func TestShouldUseLegacyOracleLOBFetchOnlyForLegacyServers(t *testing.T) {
if !shouldUseLegacyOracleLOBFetch(params, 10, true) {
t.Fatal("Oracle 10g should use streamed LOB reads")
}
if !shouldUseLegacyOracleLOBFetch(params, 11, true) {
t.Fatal("Oracle 11g should use streamed LOB reads")
}
if shouldUseLegacyOracleLOBFetch(params, 12, true) || shouldUseLegacyOracleLOBFetch(params, 19, true) {
t.Fatal("modern Oracle versions should retain the driver's default LOB mode")
if shouldUseLegacyOracleLOBFetch(params, 11, true) || shouldUseLegacyOracleLOBFetch(params, 19, true) {
t.Fatal("Oracle 11g and newer should retain the driver's default LOB mode")
}
if shouldUseLegacyOracleLOBFetch(params, 0, false) {
t.Fatal("unknown Oracle versions should retain the driver's default LOB mode")
@ -1043,6 +1076,19 @@ func TestShouldUseLegacyOracleLOBFetchOnlyForLegacyServers(t *testing.T) {
}
}
func TestOracleMethodMayReadLOB(t *testing.T) {
for _, method := range []string{"get_table_ddl", "execute_query", "execute_query_page", "start_table_read", "execute_transaction"} {
if !oracleMethodMayReadLOB(method) {
t.Fatalf("%s should enable deferred legacy LOB reads", method)
}
}
for _, method := range []string{"list_schemas", "list_tables", "list_objects", "get_columns", "list_indexes", "list_triggers"} {
if oracleMethodMayReadLOB(method) {
t.Fatalf("%s should not reconnect while loading metadata", method)
}
}
}
func TestOracleGB18030ConverterRoundTrip(t *testing.T) {
converter := oracleGB18030Converter{}
input := "DBX \u4e2d\u6587 \U00020000"