Files
MyGoNavi/internal/db/scan_rows_test.go
Syngnat ce9f5cffb9 🐛 fix(oracle): 修复大对象查询导致客户端崩溃
- 限制交互查询中 BLOB、CLOB 和 LONG 字段的预览大小
- 将默认预取行数降至 25,控制大对象批量加载内存
- 保持流式导出完整内容并补充回归测试
2026-07-28 15:28:23 +08:00

715 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"bytes"
"context"
"database/sql"
"database/sql/driver"
"fmt"
"io"
"reflect"
"strings"
"sync"
"testing"
"time"
"unicode/utf8"
)
const scanRowsDuplicateDriverName = "gonavi-scan-rows-duplicate"
const scanRowsOracleBlobTestBytes = 16*1024 + 17
var registerScanRowsDuplicateDriverOnce sync.Once
type scanRowsDuplicateDriver struct{}
func (scanRowsDuplicateDriver) Open(name string) (driver.Conn, error) {
return scanRowsDuplicateConn{}, nil
}
type scanRowsDuplicateConn struct{}
func (scanRowsDuplicateConn) Prepare(query string) (driver.Stmt, error) { return nil, driver.ErrSkip }
func (scanRowsDuplicateConn) Close() error { return nil }
func (scanRowsDuplicateConn) Begin() (driver.Tx, error) { return nil, driver.ErrSkip }
func (scanRowsDuplicateConn) QueryContext(_ context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
if query == "SELECT blob_columns" {
return &scanRowsDuplicateRows{
columns: []string{"payload"},
columnTypes: []string{"OCIBlobLocator"},
rows: [][]driver.Value{
{bytes.Repeat([]byte{0xff}, scanRowsOracleBlobTestBytes)},
},
}, nil
}
if query == "SELECT clob_columns" {
return &scanRowsDuplicateRows{
columns: []string{"content"},
columnTypes: []string{"OCIClobLocator"},
rows: [][]driver.Value{
{strings.Repeat("数", 6*1024)},
},
}, nil
}
if query == "SELECT date_columns" {
return &scanRowsDuplicateRows{
columns: []string{"ship_date", "created_at"},
columnTypes: []string{"DATE", "DATETIME"},
rows: [][]driver.Value{
{
time.Date(2025, 10, 1, 0, 0, 0, 0, time.UTC),
time.Date(2025, 10, 1, 13, 14, 15, 0, time.UTC),
},
},
}, nil
}
if query == "SELECT timestamp_columns" {
raw := buildOracleBinaryTimestamp(time.Date(2026, 6, 16, 12, 34, 56, 123456000, time.UTC))
return &scanRowsDuplicateRows{
columns: []string{"created_at"},
columnTypes: []string{"TYPE_CA"},
rows: [][]driver.Value{
{
string(raw),
},
},
}, nil
}
if query == "SELECT timestamp_precision_columns" {
raw := buildOracleBinaryTimestamp(time.Date(2026, 6, 16, 12, 34, 56, 123456000, time.UTC))
return &scanRowsDuplicateRows{
columns: []string{"created_at"},
columnTypes: []string{"TIMESTAMP(6)"},
rows: [][]driver.Value{
{
string(raw),
},
},
}, nil
}
if query == "SELECT timestamp_generic_carrier_columns" {
raw := buildOracleBinaryTimestamp(time.Date(2026, 6, 16, 12, 34, 56, 123456000, time.UTC))
return &scanRowsDuplicateRows{
columns: []string{"created_at"},
columnTypes: []string{"VARCHAR2"},
rows: [][]driver.Value{
{
string(raw),
},
},
}, nil
}
if query == "SELECT timestamp_unknown_type_columns" {
raw := buildOracleBinaryTimestamp(time.Date(2026, 6, 16, 12, 34, 56, 123456000, time.UTC))
return &scanRowsDuplicateRows{
columns: []string{"created_at"},
columnTypes: []string{""},
rows: [][]driver.Value{
{
string(raw),
},
},
}, nil
}
if query == "SELECT timestamp_mysql_encoded_columns" {
raw := buildMySQLBinaryTimestamp(time.Date(2026, 6, 16, 12, 34, 56, 123456000, time.UTC))
return &scanRowsDuplicateRows{
columns: []string{"created_at"},
columnTypes: []string{"TYPE_CA"},
rows: [][]driver.Value{
{
string(raw),
},
},
}, nil
}
if query == "SELECT timestamp_type_ca_live_columns" {
raw := []byte{20, 26, 6, 16, 16, 46, 23, 96, 196, 119, 9, 6}
return &scanRowsDuplicateRows{
columns: []string{"created_at"},
columnTypes: []string{"TYPE_CA"},
rows: [][]driver.Value{
{
string(raw),
},
},
}, nil
}
return &scanRowsDuplicateRows{
columns: []string{"id", "id", "name"},
rows: [][]driver.Value{
{int64(1), int64(2), "alice"},
},
}, nil
}
type scanRowsValueConsumer struct {
columns []string
rows [][]interface{}
}
func (c *scanRowsValueConsumer) SetColumns(columns []string) error {
c.columns = append([]string(nil), columns...)
return nil
}
func (c *scanRowsValueConsumer) ConsumeRow(row map[string]interface{}) error {
values := make([]interface{}, len(c.columns))
for index, column := range c.columns {
values[index] = row[column]
}
c.rows = append(c.rows, values)
return nil
}
func (c *scanRowsValueConsumer) ConsumeRowValues(values []interface{}) error {
c.rows = append(c.rows, append([]interface{}(nil), values...))
return nil
}
func TestScanRowsBoundsOracleBlobPreview(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open blob scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT blob_columns")
if err != nil {
t.Fatalf("query blob scan rows db failed: %v", err)
}
defer rows.Close()
// Production OracleDB leaves scanDialect empty; go-ora's column type is the
// reliable signal for applying the interactive BLOB guard.
data, columns, err := scanRowsForDialect(rows, "")
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"payload"}) || len(data) != 1 {
t.Fatalf("unexpected blob result: columns=%v rows=%d", columns, len(data))
}
preview, ok := data[0]["payload"].(string)
if !ok {
t.Fatalf("Oracle BLOB preview type = %T, want string", data[0]["payload"])
}
wantPrefix := fmt.Sprintf("[BLOB preview: 4096/%d bytes] 0x", scanRowsOracleBlobTestBytes)
if !strings.HasPrefix(preview, wantPrefix+strings.Repeat("ff", 4*1024)) {
t.Fatalf("Oracle BLOB preview is missing bounded data or visible metadata: length=%d", len(preview))
}
fullHexLength := len("0x") + scanRowsOracleBlobTestBytes*2
if len(preview) >= fullHexLength {
t.Fatalf("Oracle BLOB preview length = %d, want less than full hex length %d", len(preview), fullHexLength)
}
}
func TestStreamRowsKeepsCompleteOracleBlobValue(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open streaming blob rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT blob_columns")
if err != nil {
t.Fatalf("query streaming blob rows db failed: %v", err)
}
defer rows.Close()
consumer := &scanRowsValueConsumer{}
if err := streamRowsForDialect(rows, "", consumer); err != nil {
t.Fatalf("streamRowsForDialect returned error: %v", err)
}
if len(consumer.rows) != 1 || len(consumer.rows[0]) != 1 {
t.Fatalf("unexpected streamed blob rows: %#v", consumer.rows)
}
want := "0x" + strings.Repeat("ff", scanRowsOracleBlobTestBytes)
if got := consumer.rows[0][0]; got != want {
t.Fatalf("streamed Oracle BLOB was truncated: got length=%d want length=%d", len(got.(string)), len(want))
}
}
func TestScanRowsBoundsOracleClobPreviewAtUTF8Boundary(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open clob scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT clob_columns")
if err != nil {
t.Fatalf("query clob scan rows db failed: %v", err)
}
defer rows.Close()
data, _, err := scanRowsForDialect(rows, "")
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
preview, ok := data[0]["content"].(string)
if !ok {
t.Fatalf("Oracle CLOB preview type = %T, want string", data[0]["content"])
}
wantContentPrefix := strings.Repeat("数", (4*1024)/len("数"))
wantPrefix := fmt.Sprintf(
"[CLOB preview: %d/%d bytes] ",
len(wantContentPrefix),
len(strings.Repeat("数", 6*1024)),
)
if !strings.HasPrefix(preview, wantPrefix+wantContentPrefix) {
t.Fatalf("Oracle CLOB preview does not preserve the UTF-8 prefix: length=%d", len(preview))
}
if !utf8.ValidString(preview) {
t.Fatalf("Oracle CLOB preview split a UTF-8 character: %q", preview[:min(len(preview), 32)])
}
}
func TestStreamRowsKeepsCompleteOracleClobValue(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open streaming clob rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT clob_columns")
if err != nil {
t.Fatalf("query streaming clob rows db failed: %v", err)
}
defer rows.Close()
consumer := &scanRowsValueConsumer{}
if err := streamRowsForDialect(rows, "", consumer); err != nil {
t.Fatalf("streamRowsForDialect returned error: %v", err)
}
want := strings.Repeat("数", 6*1024)
if len(consumer.rows) != 1 || len(consumer.rows[0]) != 1 || consumer.rows[0][0] != want {
t.Fatalf("streamed Oracle CLOB was truncated: rows=%d", len(consumer.rows))
}
}
var _ driver.QueryerContext = (*scanRowsDuplicateConn)(nil)
type scanRowsDuplicateRows struct {
columns []string
columnTypes []string
rows [][]driver.Value
index int
}
func (r *scanRowsDuplicateRows) Columns() []string { return append([]string(nil), r.columns...) }
func (r *scanRowsDuplicateRows) Close() error { return nil }
func (r *scanRowsDuplicateRows) ColumnTypeDatabaseTypeName(index int) string {
if index < 0 || index >= len(r.columnTypes) {
return ""
}
return r.columnTypes[index]
}
func (r *scanRowsDuplicateRows) Next(dest []driver.Value) error {
if r.index >= len(r.rows) {
return io.EOF
}
row := r.rows[r.index]
for idx := range dest {
if idx < len(row) {
dest[idx] = row[idx]
}
}
r.index++
return nil
}
func TestScanRowsRenamesDuplicateColumns(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open duplicate scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT 1")
if err != nil {
t.Fatalf("query duplicate scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRows(rows)
if err != nil {
t.Fatalf("scanRows returned error: %v", err)
}
wantColumns := []string{"id", "id_2", "name"}
if !reflect.DeepEqual(columns, wantColumns) {
t.Fatalf("unexpected columns: got=%v want=%v", columns, wantColumns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["id"] != int64(1) || data[0]["id_2"] != int64(2) || data[0]["name"] != "alice" {
t.Fatalf("unexpected row data: %#v", data[0])
}
}
func TestScanRowsForMySQLDialectFormatsDateOnly(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open date scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT date_columns")
if err != nil {
t.Fatalf("query date scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, "mysql")
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"ship_date", "created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["ship_date"] != "2025-10-01" {
t.Fatalf("MySQL DATE 应展示为日期,实际=%v(%T)", data[0]["ship_date"], data[0]["ship_date"])
}
if data[0]["created_at"] != "2025-10-01T13:14:15Z" {
t.Fatalf("MySQL DATETIME 应保留时间,实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestScanRowsForOracleDialectKeepsDateTime(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open date scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT date_columns")
if err != nil {
t.Fatalf("query date scan rows db failed: %v", err)
}
defer rows.Close()
data, _, err := scanRowsForDialect(rows, "oracle")
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["ship_date"] != "2025-10-01T00:00:00Z" {
t.Fatalf("Oracle DATE 应保留 datetime 语义,实际=%v(%T)", data[0]["ship_date"], data[0]["ship_date"])
}
}
func TestScanRowsForOceanBaseOracleDialectFormatsMidnightDateOnly(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open date scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT date_columns")
if err != nil {
t.Fatalf("query date scan rows db failed: %v", err)
}
defer rows.Close()
data, _, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["ship_date"] != "2025-10-01" {
t.Fatalf("OceanBase Oracle DATE 的午夜值应展示为日期,实际=%v(%T)", data[0]["ship_date"], data[0]["ship_date"])
}
if data[0]["created_at"] != "2025-10-01T13:14:15Z" {
t.Fatalf("OceanBase Oracle DATETIME 应保留时间,实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestOracleDBQueryUsesCustomScanDialect(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open date scan rows db failed: %v", err)
}
defer dbConn.Close()
oracleDB := &OracleDB{conn: dbConn, scanDialect: oceanBaseOracleScanDialect}
data, _, err := oracleDB.Query("SELECT date_columns")
if err != nil {
t.Fatalf("OracleDB.Query returned error: %v", err)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["ship_date"] != "2025-10-01" {
t.Fatalf("OracleDB 自定义扫描方言未生效,实际=%v(%T)", data[0]["ship_date"], data[0]["ship_date"])
}
}
func TestScanRowsForOceanBaseOracleDialectDecodesBinaryTimestampString(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open timestamp scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT timestamp_columns")
if err != nil {
t.Fatalf("query timestamp scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["created_at"] != "2026-06-16T12:34:56.123456Z" {
t.Fatalf("OceanBase Oracle 二进制 TIMESTAMP 应解码为 RFC3339实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestScanRowsForOceanBaseOracleDialectDecodesBinaryTimestampStringWithPrecisionType(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open timestamp precision scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT timestamp_precision_columns")
if err != nil {
t.Fatalf("query timestamp precision scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["created_at"] != "2026-06-16T12:34:56.123456Z" {
t.Fatalf("OceanBase Oracle TIMESTAMP(6) 应解码为 RFC3339实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestScanRowsForOceanBaseOracleDialectDecodesBinaryTimestampStringWithGenericCarrierType(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open timestamp generic-carrier scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT timestamp_generic_carrier_columns")
if err != nil {
t.Fatalf("query timestamp generic-carrier scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["created_at"] != "2026-06-16T12:34:56.123456Z" {
t.Fatalf("OceanBase Oracle 泛型载体类型的 TIMESTAMP 应解码为 RFC3339实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestScanRowsForOceanBaseOracleDialectDecodesBinaryTimestampStringWithoutTypeName(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open timestamp unknown-type scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT timestamp_unknown_type_columns")
if err != nil {
t.Fatalf("query timestamp unknown-type scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["created_at"] != "2026-06-16T12:34:56.123456Z" {
t.Fatalf("OceanBase Oracle 空类型名的 TIMESTAMP 应解码为 RFC3339实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestScanRowsForOceanBaseOracleDialectDecodesMySQLLengthEncodedTimestampString(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open mysql-encoded timestamp scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT timestamp_mysql_encoded_columns")
if err != nil {
t.Fatalf("query mysql-encoded timestamp scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["created_at"] != "2026-06-16T12:34:56.123456Z" {
t.Fatalf("OceanBase Oracle length-encoded TIMESTAMP 应解码为 RFC3339实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}
func TestScanRowsForOceanBaseOracleDialectDecodesTypeCALiveTimestampString(t *testing.T) {
t.Parallel()
registerScanRowsDuplicateDriverOnce.Do(func() {
sql.Register(scanRowsDuplicateDriverName, scanRowsDuplicateDriver{})
})
dbConn, err := sql.Open(scanRowsDuplicateDriverName, "")
if err != nil {
t.Fatalf("open timestamp scan rows db failed: %v", err)
}
defer dbConn.Close()
rows, err := dbConn.QueryContext(context.Background(), "SELECT timestamp_type_ca_live_columns")
if err != nil {
t.Fatalf("query timestamp scan rows db failed: %v", err)
}
defer rows.Close()
data, columns, err := scanRowsForDialect(rows, oceanBaseOracleScanDialect)
if err != nil {
t.Fatalf("scanRowsForDialect returned error: %v", err)
}
if !reflect.DeepEqual(columns, []string{"created_at"}) {
t.Fatalf("unexpected columns: %v", columns)
}
if len(data) != 1 {
t.Fatalf("expected one row, got=%d", len(data))
}
if data[0]["created_at"] != "2026-06-16T16:46:23.158844Z" {
t.Fatalf("OceanBase Oracle TYPE_CA live TIMESTAMP 应解码为 RFC3339实际=%v(%T)", data[0]["created_at"], data[0]["created_at"])
}
}