diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go index 0f6529923..39d24ef5e 100644 --- a/agents/drivers/oracle-go/main.go +++ b/agents/drivers/oracle-go/main.go @@ -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 } diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go index 4babd4e82..a0dec1a16 100644 --- a/agents/drivers/oracle-go/main_test.go +++ b/agents/drivers/oracle-go/main_test.go @@ -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"