dbx/agents/drivers/oracle-go/main_test.go

1351 lines
47 KiB
Go

package main
import (
"context"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"io"
"net/url"
"os"
"reflect"
"strings"
"testing"
"time"
)
func TestHandshakeResponse(t *testing.T) {
s := newServer()
resp, shutdown := s.handleLine(`{"jsonrpc":"2.0","id":7,"method":"handshake","params":{"appVersion":"dev"}}`)
if shutdown {
t.Fatal("handshake should not shut down the server")
}
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error)
}
data, err := json.Marshal(resp.Result)
if err != nil {
t.Fatal(err)
}
var result struct {
ProtocolVersion int `json:"protocolVersion"`
AgentProtocolVersion int `json:"agentProtocolVersion"`
Capabilities []string `json:"capabilities"`
}
if err := json.Unmarshal(data, &result); err != nil {
t.Fatal(err)
}
if result.ProtocolVersion != 1 || result.AgentProtocolVersion != 1 {
t.Fatalf("unexpected protocol versions: %+v", result)
}
contract := protocolContract(t)
if result.ProtocolVersion != contract.ProtocolVersion || result.AgentProtocolVersion != contract.ProtocolVersion {
t.Fatalf("handshake protocol versions do not match contract: result=%+v contract=%+v", result, contract)
}
for _, capability := range result.Capabilities {
if !contains(contract.AllCapabilities, capability) {
t.Fatalf("handshake returned capability %q outside protocol contract %v", capability, contract.AllCapabilities)
}
}
if !contains(result.Capabilities, "query") || !contains(result.Capabilities, "metadata") {
t.Fatalf("expected query and metadata capabilities, got %v", result.Capabilities)
}
}
func TestRuntimeHandshakeAdvertisesMultiSessionProtocol(t *testing.T) {
runtime := newRuntimeServer()
resp, shutdown := runtime.handleLine(`{"jsonrpc":"2.0","id":7,"method":"handshake","params":{"appVersion":"dev"}}`)
if shutdown || resp.Error != nil {
t.Fatalf("unexpected handshake response: shutdown=%v error=%v", shutdown, resp.Error)
}
data, err := json.Marshal(resp.Result)
if err != nil {
t.Fatal(err)
}
var result struct {
ProtocolVersion int `json:"protocolVersion"`
AgentProtocolVersion int `json:"agentProtocolVersion"`
Capabilities []string `json:"capabilities"`
}
if err := json.Unmarshal(data, &result); err != nil {
t.Fatal(err)
}
if result.ProtocolVersion != 2 || result.AgentProtocolVersion != 2 {
t.Fatalf("unexpected protocol versions: %+v", result)
}
if !contains(result.Capabilities, "multi_session") {
t.Fatalf("expected multi_session capability, got %v", result.Capabilities)
}
}
func TestRuntimeMissingAgentSessionDoesNotUseQueryCursorSessionID(t *testing.T) {
runtime := newRuntimeServer()
resp, shutdown := runtime.handleLine(`{"jsonrpc":"2.0","id":8,"method":"fetch_query_page","params":{"sessionId":"cursor-1","pageSize":10}}`)
if shutdown {
t.Fatal("fetch_query_page should not shut down the runtime")
}
if resp.Error == nil || !strings.Contains(resp.Error.Message, legacyAgentSessionID) {
t.Fatalf("expected missing legacy agent session error, got %#v", resp.Error)
}
}
func TestRuntimeCloseOneSessionKeepsOtherSessionRegistered(t *testing.T) {
runtime := newRuntimeServer()
runtime.sessions["a"] = &agentSession{server: newServer()}
runtime.sessions["b"] = &agentSession{server: newServer()}
if err := runtime.closeSession("a"); err != nil {
t.Fatal(err)
}
if _, err := runtime.session("a"); err == nil {
t.Fatal("closed session should be removed")
}
if _, err := runtime.session("b"); err != nil {
t.Fatalf("other session should remain registered: %v", err)
}
}
func TestRuntimeCancelSessionOnlyCancelsTargetSession(t *testing.T) {
runtime := newRuntimeServer()
serverA := newServer()
serverB := newServer()
ctxA, cancelA := context.WithCancel(context.Background())
ctxB, cancelB := context.WithCancel(context.Background())
serverA.activeCancel = cancelA
serverB.activeCancel = cancelB
runtime.sessions["a"] = &agentSession{server: serverA}
runtime.sessions["b"] = &agentSession{server: serverB}
resp, shutdown := runtime.handleLine(`{"jsonrpc":"2.0","id":9,"method":"cancel_session","params":{"agentSessionId":"a"}}`)
if shutdown || resp.Error != nil {
t.Fatalf("unexpected cancel response: shutdown=%v error=%v", shutdown, resp.Error)
}
select {
case <-ctxA.Done():
default:
t.Fatal("target session was not canceled")
}
select {
case <-ctxB.Done():
t.Fatal("canceling session a should not cancel session b")
default:
}
cancelB()
}
func TestCloseMissingQuerySessionReturnsFalse(t *testing.T) {
s := newServer()
resp, shutdown := s.handleLine(`{"jsonrpc":"2.0","id":8,"method":"close_query_session","params":{"sessionId":"missing"}}`)
if shutdown {
t.Fatal("close_query_session should not shut down the server")
}
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error)
}
if resp.Result != false {
t.Fatalf("expected false result, got %#v", resp.Result)
}
}
func TestMissingTableReadSessionMethodsReturnEmptyOrFalse(t *testing.T) {
s := newServer()
fetchResp, shutdown := s.handleLine(`{"jsonrpc":"2.0","id":9,"method":"fetch_table_read_page","params":{"sessionId":"missing","pageSize":10}}`)
if shutdown {
t.Fatal("fetch_table_read_page should not shut down the server")
}
if fetchResp.Error != nil {
t.Fatalf("unexpected fetch error: %v", fetchResp.Error)
}
data, err := json.Marshal(fetchResp.Result)
if err != nil {
t.Fatal(err)
}
var page queryPageResult
if err := json.Unmarshal(data, &page); err != nil {
t.Fatal(err)
}
if len(page.Columns) != 0 || len(page.ColumnTypes) != 0 || len(page.Rows) != 0 || page.HasMore || page.SessionID != nil {
t.Fatalf("missing table read session should return empty page, got %+v", page)
}
closeResp, shutdown := s.handleLine(`{"jsonrpc":"2.0","id":10,"method":"close_table_read_session","params":{"sessionId":"missing"}}`)
if shutdown {
t.Fatal("close_table_read_session should not shut down the server")
}
if closeResp.Error != nil {
t.Fatalf("unexpected close error: %v", closeResp.Error)
}
if closeResp.Result != false {
t.Fatalf("expected false result, got %#v", closeResp.Result)
}
}
func TestEmptyResultSlicesMarshalAsArrays(t *testing.T) {
data, err := json.Marshal(queryResult{})
if err != nil {
t.Fatal(err)
}
text := string(data)
if strings.Contains(text, `"columns":null`) || strings.Contains(text, `"column_types":null`) || strings.Contains(text, `"rows":null`) {
t.Fatalf("query result should marshal nil slices as arrays: %s", text)
}
if !strings.Contains(text, `"column_types":[]`) {
t.Fatalf("query result should marshal empty column types array: %s", text)
}
data, err = json.Marshal(indexInfo{})
if err != nil {
t.Fatal(err)
}
text = string(data)
if strings.Contains(text, `"columns":null`) || strings.Contains(text, `"included_columns":null`) {
t.Fatalf("index info should marshal nil slices as arrays: %s", text)
}
}
func TestGetTableDDLResultMarshalsAsString(t *testing.T) {
data, err := json.Marshal("CREATE TABLE HR.ORDERS (ID NUMBER)")
if err != nil {
t.Fatal(err)
}
var ddl string
if err := json.Unmarshal(data, &ddl); err != nil {
t.Fatalf("get_table_ddl result must deserialize as a string: %v", err)
}
}
func TestNormalizeValueFormatsOracleBinaryColumnsAsHex(t *testing.T) {
tests := map[string]string{
"RAW": "0x000f10ff",
"raw": "0x000f10ff",
"LongRaw": "0x000f10ff",
"LONG RAW": "0x000f10ff",
"LongVarRaw": "0x000f10ff",
"OCIBlobLocator": "0x000f10ff",
}
for columnType, want := range tests {
if got := normalizeValue([]byte{0x00, 0x0f, 0x10, 0xff}, columnType); got != want {
t.Fatalf("normalizeValue RAW bytes for %q = %#v, want %q", columnType, got, want)
}
}
}
func TestNormalizeValueKeepsNonBinaryBytesAsText(t *testing.T) {
if got := normalizeValue([]byte("hello"), "VARCHAR2"); got != "hello" {
t.Fatalf("normalizeValue text bytes = %#v, want %q", got, "hello")
}
if got := normalizeValue([]byte("legacy"), ""); got != "legacy" {
t.Fatalf("normalizeValue bytes without metadata = %#v, want %q", got, "legacy")
}
}
func TestNormalizeValueFormatsOracleTimezoneLessDateTimesAsWallClock(t *testing.T) {
value := time.Date(2026, time.July, 23, 13, 42, 13, 123456000, time.FixedZone("CST", 8*60*60))
tests := []string{
"DATE",
"TIMESTAMP",
"TIMESTAMP(6)",
"TimeStampDTY",
}
for _, columnType := range tests {
if got := normalizeValue(value, columnType); got != "2026-07-23T13:42:13.123456" {
t.Fatalf("normalizeValue time for %q = %#v, want wall-clock value", columnType, got)
}
}
}
func TestNormalizeValueKeepsOracleZonedDateTimeOffsets(t *testing.T) {
value := time.Date(2026, time.July, 23, 13, 42, 13, 123456000, time.FixedZone("CST", 8*60*60))
tests := []string{
"TimeStampTZ_DTY",
"TIMESTAMP WITH TIME ZONE",
}
for _, columnType := range tests {
if got := normalizeValue(value, columnType); got != "2026-07-23T13:42:13.123456+08:00" {
t.Fatalf("normalizeValue time for %q = %#v, want RFC3339 offset", columnType, got)
}
}
}
func TestNormalizeDDLObjectType(t *testing.T) {
tests := map[string]string{
"": "",
"table": "TABLE",
"VIEW": "VIEW",
"materialized view": "MATERIALIZED_VIEW",
"MATERIALIZED_VIEW": "MATERIALIZED_VIEW",
"procedure": "",
}
for input, want := range tests {
if got := normalizeDDLObjectType(input); got != want {
t.Fatalf("normalizeDDLObjectType(%q) = %q, want %q", input, got, want)
}
}
}
func TestIsQuerySQLSkipsLeadingComments(t *testing.T) {
tests := []string{
"-- 测试\nSELECT * FROM (SELECT * FROM \"DBX_TEST\".\"ORDERS_10K\") WHERE ROWNUM <= 100",
"/* explain */\nSELECT * FROM dual",
"-- comment\r\nWITH rows AS (SELECT 1 FROM dual) SELECT * FROM rows",
}
for _, sqlText := range tests {
if !isQuerySQL(sqlText) {
t.Fatalf("expected SQL to be treated as query: %s", sqlText)
}
}
}
func TestIsQuerySQLRequiresKeywordBoundary(t *testing.T) {
tests := []string{
"-- comment only",
"selectivity FROM stats",
"withdraw FROM account",
"/* unterminated comment",
}
for _, sqlText := range tests {
if isQuerySQL(sqlText) {
t.Fatalf("expected SQL not to be treated as query: %s", sqlText)
}
}
}
func TestTrimStatementSQLPreservesAnonymousPLSQLBlockTerminator(t *testing.T) {
sqlText := `DECLARE
PRE_TRD_DATE INTEGER ;
BEGIN
SELECT 1 + 2 INTO PRE_TRD_DATE FROM DUAL;
END;`
if got := trimStatementSQL(sqlText); got != sqlText {
t.Fatalf("trimStatementSQL() = %q, want full PL/SQL block %q", got, sqlText)
}
}
func TestTrimStatementSQLStripsSlashDelimiterAfterPLSQLBlock(t *testing.T) {
sqlText := "BEGIN\n NULL;\nEND;\n/"
want := "BEGIN\n NULL;\nEND;"
if got := trimStatementSQL(sqlText); got != want {
t.Fatalf("trimStatementSQL() = %q, want %q", got, want)
}
}
func TestTrimStatementSQLPreservesCreatePLSQLObjectTerminator(t *testing.T) {
tests := []string{
"CREATE OR REPLACE PROCEDURE p AS\nBEGIN\n NULL;\nEND;",
"CREATE OR REPLACE FUNCTION f RETURN NUMBER AS\nBEGIN\n RETURN 1;\nEND;",
"CREATE OR REPLACE PACKAGE pkg_utils AS\n FUNCTION get_version RETURN VARCHAR2;\nEND pkg_utils;",
}
for _, sqlText := range tests {
if got := trimStatementSQL(sqlText); got != sqlText {
t.Fatalf("trimStatementSQL() = %q, want full PL/SQL object %q", got, sqlText)
}
}
}
func TestTrimStatementSQLStripsSlashDelimiterAfterCreatePLSQLObject(t *testing.T) {
sqlText := "CREATE OR REPLACE PROCEDURE p AS\nBEGIN\n NULL;\nEND;\n/"
want := "CREATE OR REPLACE PROCEDURE p AS\nBEGIN\n NULL;\nEND;"
if got := trimStatementSQL(sqlText); got != want {
t.Fatalf("trimStatementSQL() = %q, want %q", got, want)
}
}
func TestTrimStatementSQLRemovesRegularStatementSemicolon(t *testing.T) {
if got := trimStatementSQL("SELECT 1 FROM DUAL;"); got != "SELECT 1 FROM DUAL" {
t.Fatalf("trimStatementSQL() = %q, want regular statement without semicolon", got)
}
}
func TestOracleExplainPlanBindParamsIncludesNamedParameters(t *testing.T) {
sqlText := `
SELECT *
FROM orders
WHERE id = :id
AND status = :status
AND parent_id = :id`
want := []oracleBindParam{
{Name: "id"},
{Name: "status"},
}
if got := oracleExplainPlanBindParams(sqlText); !reflect.DeepEqual(got, want) {
t.Fatalf("oracleExplainPlanBindParams() = %#v, want %#v", got, want)
}
}
func TestOracleExplainPlanBindParamsSkipsQuotedTextAndComments(t *testing.T) {
sqlText := `
SELECT ':literal' AS literal_value,
q'[not :q_param]' AS q_literal,
"COL:NAME" AS quoted_identifier
FROM orders
WHERE id = :id
-- ignored :comment_param
AND note <> 'escaped '' :text_param'
/* ignored :block_param */`
want := []oracleBindParam{{Name: "id"}}
if got := oracleExplainPlanBindParams(sqlText); !reflect.DeepEqual(got, want) {
t.Fatalf("oracleExplainPlanBindParams() = %#v, want %#v", got, want)
}
}
func TestOracleExplainPlanBindParamsIncludesPositionalParameters(t *testing.T) {
sqlText := "SELECT * FROM orders WHERE id = :1 AND status = :status"
want := []oracleBindParam{
{Name: "1", Positional: true},
{Name: "status"},
}
if got := oracleExplainPlanBindParams(sqlText); !reflect.DeepEqual(got, want) {
t.Fatalf("oracleExplainPlanBindParams() = %#v, want %#v", got, want)
}
}
func TestOracleExplainPlanBindArgsUsesNamedArguments(t *testing.T) {
args := oracleExplainPlanBindArgs("SELECT * FROM orders WHERE id = :id")
if len(args) != 1 {
t.Fatalf("expected one bind argument, got %#v", args)
}
named, ok := args[0].(sql.NamedArg)
if !ok {
t.Fatalf("expected sql.NamedArg, got %#v", args[0])
}
if named.Name != "id" || named.Value != nil {
t.Fatalf("unexpected named bind argument: %#v", named)
}
}
func protocolContract(t *testing.T) struct {
ProtocolVersion int `json:"protocolVersion"`
AllCapabilities []string `json:"allCapabilities"`
} {
t.Helper()
data, err := os.ReadFile("../../common/src/main/resources/agent-protocol-v1.json")
if err != nil {
t.Fatal(err)
}
var contract struct {
ProtocolVersion int `json:"protocolVersion"`
AllCapabilities []string `json:"allCapabilities"`
}
if err := json.Unmarshal(data, &contract); err != nil {
t.Fatal(err)
}
return contract
}
func TestOracleColumnTypeDDL(t *testing.T) {
charLen := 64
precision := 10
scale := 2
zeroScale := 0
tests := []struct {
name string
column columnInfo
want string
}{
{name: "varchar", column: columnInfo{DataType: "VARCHAR2", CharacterMaximumLength: &charLen}, want: "VARCHAR2(64)"},
{name: "number scale", column: columnInfo{DataType: "NUMBER", NumericPrecision: &precision, NumericScale: &scale}, want: "NUMBER(10,2)"},
{name: "number zero scale", column: columnInfo{DataType: "NUMBER", NumericPrecision: &precision, NumericScale: &zeroScale}, want: "NUMBER(10)"},
{name: "timestamp preserves precision", column: columnInfo{DataType: "TIMESTAMP(6)"}, want: "TIMESTAMP(6)"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := oracleColumnTypeDDL(tt.column); got != tt.want {
t.Fatalf("oracleColumnTypeDDL() = %q, want %q", got, tt.want)
}
})
}
}
func TestBuildDSNUsesConnectionStringWhenProvided(t *testing.T) {
dsn := buildDSN(connectParams{ConnectionString: "oracle://scott:tiger@db.example.com:1521/ORCLPDB1"})
if dsn != "oracle://scott:tiger@db.example.com:1521/ORCLPDB1" {
t.Fatalf("unexpected dsn: %s", dsn)
}
}
func TestBuildDSNPreservesBastionUsernameAndEncodesCredentials(t *testing.T) {
dsn := buildDSN(connectParams{
Host: "db.example.com",
Port: 1521,
Database: "XE",
Username: "9008888:reader",
Password: "dbx:pass",
})
parsed, err := url.Parse(dsn)
if err != nil {
t.Fatal(err)
}
password, _ := parsed.User.Password()
if parsed.User.Username() != "9008888:reader" || password != "dbx:pass" {
t.Fatalf("credentials should survive URL parsing, dsn=%s username=%q password=%q", dsn, parsed.User.Username(), password)
}
if !strings.HasPrefix(parsed.User.String(), "9008888%3Areader:") {
t.Fatalf("bastion username should be escaped without being quoted, dsn=%s", dsn)
}
}
func TestBuildDSNEncodesColonInCredentialsFromJDBCServiceURL(t *testing.T) {
dsn := buildDSN(connectParams{
Username: "9008888:reader",
Password: "dbx:pass",
ConnectionString: "jdbc:oracle:thin:@//db.example.com:1521/XE",
})
parsed, err := url.Parse(dsn)
if err != nil {
t.Fatal(err)
}
password, _ := parsed.User.Password()
if parsed.User.Username() != "9008888:reader" || password != "dbx:pass" {
t.Fatalf("credentials should survive JDBC URL conversion, dsn=%s username=%q password=%q", dsn, parsed.User.Username(), password)
}
if parsed.Host != "db.example.com:1521" || strings.TrimPrefix(parsed.Path, "/") != "XE" {
t.Fatalf("JDBC host/service should survive conversion, dsn=%s", dsn)
}
}
func TestBuildDSNPreservesExplicitlyQuotedUsername(t *testing.T) {
dsn := buildDSN(connectParams{
Host: "db.example.com",
Port: 1521,
Database: "XE",
Username: `"abc:def"`,
Password: "dbx:pass",
})
parsed, err := url.Parse(dsn)
if err != nil {
t.Fatal(err)
}
if parsed.User.Username() != `"abc:def"` {
t.Fatalf("explicitly quoted username should remain unchanged, dsn=%s username=%q", dsn, parsed.User.Username())
}
}
func TestBuildDSNUsesJdbcServiceHostAndPort(t *testing.T) {
dsn := buildDSN(connectParams{
Host: "127.0.0.1",
Port: 11521,
Database: "ORCLPDB1",
Username: "scott",
Password: "tiger",
ConnectionString: "jdbc:oracle:thin:@//oracle.example.com:1521/ORCLPDB1",
})
if strings.Contains(strings.ToLower(dsn), "jdbc:") {
t.Fatalf("dsn should be go-ora format, got: %s", dsn)
}
if !strings.Contains(dsn, "oracle.example.com:1521") || !strings.Contains(dsn, "ORCLPDB1") {
t.Fatalf("dsn should use JDBC host/port/database fields, got: %s", dsn)
}
}
func TestBuildDSNUsesRewrittenJdbcServiceHostAndPort(t *testing.T) {
dsn := buildDSN(connectParams{
Host: "127.0.0.1",
Port: 11521,
Database: "ORCLPDB1",
Username: "scott",
Password: "tiger",
ConnectionString: "jdbc:oracle:thin:@//127.0.0.1:11521/ORCLPDB1",
})
if strings.Contains(strings.ToLower(dsn), "jdbc:") {
t.Fatalf("dsn should be go-ora format, got: %s", dsn)
}
if !strings.Contains(dsn, "127.0.0.1:11521") || !strings.Contains(dsn, "ORCLPDB1") {
t.Fatalf("dsn should use rewritten JDBC host/port/database fields, got: %s", dsn)
}
}
func TestBuildDSNConvertsJdbcSID(t *testing.T) {
dsn := buildDSN(connectParams{
Host: "127.0.0.1",
Port: 11521,
Database: "ORCL",
Username: "scott",
Password: "tiger",
ConnectionString: "jdbc:oracle:thin:@oracle.example.com:1521:ORCL",
})
if strings.Contains(strings.ToLower(dsn), "jdbc:") {
t.Fatalf("dsn should be go-ora format, got: %s", dsn)
}
upperDSN := strings.ToUpper(dsn)
if !strings.Contains(dsn, "oracle.example.com:1521") || !strings.Contains(upperDSN, "SID=ORCL") {
t.Fatalf("dsn should use JDBC host/port and SID option, got: %s", dsn)
}
}
func TestBuildDSNConvertsJdbcDescriptor(t *testing.T) {
dsn := buildDSN(connectParams{
Username: "scott",
Password: "tiger",
ConnectionString: "jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=db.example.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCLPDB1)))",
})
if !strings.HasPrefix(dsn, "oracle://scott:tiger@") {
t.Fatalf("descriptor should become go-ora url, got: %s", dsn)
}
if !strings.Contains(dsn, "connStr=") {
t.Fatalf("descriptor should be passed via connStr option, got: %s", dsn)
}
}
func TestBuildDSNAddsSysDbaOption(t *testing.T) {
dsn := buildDSN(connectParams{
Host: "127.0.0.1",
Port: 1521,
Database: "SYSDBA:ORCLPDB1",
Username: "sys",
Password: "secret",
SysDBA: true,
URLParams: "TRACE FILE=trace.log",
})
if strings.Contains(dsn, "SYSDBA:") {
t.Fatalf("dsn should strip SYSDBA prefix: %s", dsn)
}
if !strings.Contains(dsn, "ORCLPDB1") {
t.Fatalf("dsn should include service name: %s", dsn)
}
upperDSN := strings.ToUpper(dsn)
if !strings.Contains(upperDSN, "AUTH TYPE=SYSDBA") &&
!strings.Contains(upperDSN, "AUTH+TYPE=SYSDBA") &&
!strings.Contains(upperDSN, "AUTH%20TYPE=SYSDBA") {
t.Fatalf("dsn should include SYSDBA auth option: %s", dsn)
}
}
func TestOracleGB18030ConverterRoundTrip(t *testing.T) {
converter := oracleGB18030Converter{}
input := "DBX \u4e2d\u6587 \U00020000"
encoded := converter.Encode(input)
if string(encoded) == input {
t.Fatalf("GB18030 converter should encode non-ASCII text away from UTF-8 bytes")
}
if decoded := converter.Decode(encoded); decoded != input {
t.Fatalf("GB18030 round trip = %q, want %q", decoded, input)
}
if converter.GetLangID() != oracleCharsetZHS32GB18030 {
t.Fatalf("GB18030 converter lang id = %d, want %d", converter.GetLangID(), oracleCharsetZHS32GB18030)
}
if clone := converter.Clone(); clone.GetLangID() != oracleCharsetZHS32GB18030 {
t.Fatalf("GB18030 converter clone lang id = %d, want %d", clone.GetLangID(), oracleCharsetZHS32GB18030)
}
}
func TestOracleStringConverterForUnsupportedCharsetError(t *testing.T) {
err := errors.New("the server use charset with id: 854 which is not supported by the driver")
converter, ok := oracleStringConverterForUnsupportedCharsetError(err)
if !ok {
t.Fatalf("expected GB18030 server charset error to have a converter")
}
if converter.GetLangID() != oracleCharsetZHS32GB18030 {
t.Fatalf("converter lang id = %d, want %d", converter.GetLangID(), oracleCharsetZHS32GB18030)
}
ncharsetErr := errors.New("the server use ncharset with id: 854 which is not supported by the driver")
if _, ok := oracleStringConverterForUnsupportedCharsetError(ncharsetErr); ok {
t.Fatalf("ncharset errors should not have a server charset converter")
}
otherCharsetErr := errors.New("the server use charset with id: 852 which is not supported by the driver")
if charsetID, ok := unsupportedOracleServerCharsetID(otherCharsetErr); !ok || charsetID != 852 {
t.Fatalf("other server charset should still be parsed, got id=%d ok=%v", charsetID, ok)
}
if _, ok := oracleStringConverterForUnsupportedCharsetError(otherCharsetErr); ok {
t.Fatalf("unknown charset ids should not get a guessed converter")
}
}
func TestListDatabasesSQLUsesUserDictionaryInsteadOfObjectDictionary(t *testing.T) {
sqlText := strings.ToUpper(oracleListDatabasesSQL)
if !strings.Contains(sqlText, "ALL_USERS") {
t.Fatalf("database listing should query ALL_USERS, got: %s", oracleListDatabasesSQL)
}
if strings.Contains(sqlText, "ALL_TABLES") || strings.Contains(sqlText, "ALL_VIEWS") {
t.Fatalf("database listing should not scan object dictionaries, got: %s", oracleListDatabasesSQL)
}
if strings.Contains(sqlText, "'DIP'") {
t.Fatalf("database listing should not hide an existing user named DIP, got: %s", oracleListDatabasesSQL)
}
if !strings.Contains(sqlText, "'SYS','SYSTEM'") || !strings.Contains(sqlText, "USERNAME NOT LIKE 'APEX_%'") {
t.Fatalf("database listing should retain system schema filtering, got: %s", oracleListDatabasesSQL)
}
}
func TestListDatabasesSQLCanApplyVisibleSchemaFilter(t *testing.T) {
sqlText, args := oracleListDatabasesSQLWithVisibleSchemas([]string{"APP", "REPORTING"})
upperSQL := strings.ToUpper(sqlText)
if !strings.Contains(upperSQL, "ALL_USERS") {
t.Fatalf("database listing should query ALL_USERS, got: %s", sqlText)
}
if !strings.Contains(upperSQL, "USERNAME IN (:1,:2)") {
t.Fatalf("database listing should apply visible schema filter, got: %s", sqlText)
}
if len(args) != 2 || args[0] != "APP" || args[1] != "REPORTING" {
t.Fatalf("visible schema args were not preserved: %#v", args)
}
if strings.Contains(upperSQL, "ALL_TABLES") || strings.Contains(upperSQL, "ALL_VIEWS") {
t.Fatalf("database listing should not scan object dictionaries, got: %s", sqlText)
}
if !strings.Contains(upperSQL, "'SYS','SYSTEM'") {
t.Fatalf("database visible-schema filtering should retain system exclusions, got: %s", sqlText)
}
}
func TestListSchemasSQLIncludesSystemUsersWithoutSynthesizingPublic(t *testing.T) {
sqlText := strings.ToUpper(oracleListSchemasSQL)
if !strings.Contains(sqlText, "FROM ALL_USERS") {
t.Fatalf("schema listing should query ALL_USERS, got: %s", oracleListSchemasSQL)
}
if strings.Contains(sqlText, "NOT IN") || strings.Contains(sqlText, "'SYS'") || strings.Contains(sqlText, "'SYSTEM'") {
t.Fatalf("schema listing should not hard-exclude SYS or SYSTEM, got: %s", oracleListSchemasSQL)
}
if strings.Contains(sqlText, "PUBLIC") || strings.Contains(sqlText, "UNION") {
t.Fatalf("schema listing should not synthesize PUBLIC, got: %s", oracleListSchemasSQL)
}
if !strings.Contains(sqlText, "CURRENT_SCHEMA') THEN 0") || !strings.Contains(sqlText, "SESSION_USER') THEN 1") {
t.Fatalf("schema listing should prioritize CURRENT_SCHEMA then SESSION_USER, got: %s", oracleListSchemasSQL)
}
}
func TestListSchemasSQLCanApplyVisibleSchemaFilter(t *testing.T) {
sqlText, args := oracleListSchemasSQLWithVisibleSchemas([]string{"SYS", "SYSTEM"})
upperSQL := strings.ToUpper(sqlText)
if !strings.Contains(upperSQL, "USERNAME IN (:1,:2)") {
t.Fatalf("schema listing should parameterize visible schemas, got: %s", sqlText)
}
if len(args) != 2 || args[0] != "SYS" || args[1] != "SYSTEM" {
t.Fatalf("visible schema args were not preserved: %#v", args)
}
if strings.Contains(upperSQL, "NOT IN") || strings.Contains(upperSQL, "'SYS'") || strings.Contains(upperSQL, "'SYSTEM'") {
t.Fatalf("visible schema query should not hard-exclude SYS or SYSTEM, got: %s", sqlText)
}
if !strings.Contains(upperSQL, "CURRENT_SCHEMA') THEN 0") || !strings.Contains(upperSQL, "SESSION_USER') THEN 1") {
t.Fatalf("visible schema query should preserve schema ordering, got: %s", sqlText)
}
}
func TestResolveOracleSchemaPrefersCurrentSchemaOverSessionUser(t *testing.T) {
currentCalls := 0
sessionUserCalls := 0
schema, err := resolveOracleSchema(
"",
func() (string, error) {
currentCalls++
return "REPORTING", nil
},
func() (string, error) {
sessionUserCalls++
return "APP", nil
},
)
if err != nil || schema != "REPORTING" {
t.Fatalf("resolved schema = %q, err = %v; want REPORTING", schema, err)
}
if currentCalls != 1 || sessionUserCalls != 0 {
t.Fatalf("unexpected resolver calls: current=%d session_user=%d", currentCalls, sessionUserCalls)
}
}
func TestResolveOracleSchemaFallsBackToSessionUser(t *testing.T) {
schema, err := resolveOracleSchema(
"",
func() (string, error) { return "", errors.New("CURRENT_SCHEMA unavailable") },
func() (string, error) { return "APP", nil },
)
if err != nil || schema != "APP" {
t.Fatalf("resolved schema = %q, err = %v; want APP", schema, err)
}
}
func TestListTablesSQLUsesSplitDictionaryQuery(t *testing.T) {
sqlText := strings.ToUpper(oracleListTablesSQL)
if !strings.Contains(sqlText, "ALL_TABLES") || !strings.Contains(sqlText, "ALL_OBJECTS") {
t.Fatalf("table listing should split tables and views, got: %s", oracleListTablesSQL)
}
if !strings.Contains(sqlText, "UNION ALL") {
t.Fatalf("table listing should union table and view metadata, got: %s", oracleListTablesSQL)
}
if strings.Contains(sqlText, "ALL_TAB_COMMENTS") {
t.Fatalf("table listing should not load comments during refresh, got: %s", oracleListTablesSQL)
}
}
func TestListTablesQueryAppliesMetadataConstraints(t *testing.T) {
query := oracleListTablesQuery("APP", metadataListConstraints{
Filter: "u_r",
Limit: 501,
Offset: 10,
ObjectTypes: []string{"view", "TABLE", "TABLE"},
})
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE :3 ESCAPE '\\'") {
t.Fatalf("table listing should push filter predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "TABLE_TYPE IN (:4,:5)") {
t.Fatalf("table listing should push table type predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "ROWNUM <= :6") || !strings.Contains(sqlText, "DBX_RN > :7") {
t.Fatalf("table listing should use rownum pagination, got: %s", query.SQL)
}
if len(query.Args) != 7 {
t.Fatalf("unexpected args: %#v", query.Args)
}
if query.Args[0] != "APP" || query.Args[1] != "APP" || query.Args[2] != "%U%\\_%R%" || query.Args[3] != "TABLE" || query.Args[4] != "VIEW" || query.Args[5] != 511 || query.Args[6] != 10 {
t.Fatalf("constraints args were not normalized: %#v", query.Args)
}
}
func TestListSessionUserTablesQueryUsesUserDictionary(t *testing.T) {
query := oracleListSessionUserTablesQuery(metadataListConstraints{
Filter: "u_r",
Limit: 501,
Offset: 10,
ObjectTypes: []string{"view", "TABLE", "TABLE"},
})
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "USER_TABLES") || !strings.Contains(sqlText, "USER_OBJECTS") {
t.Fatalf("session-user table listing should use USER_* dictionaries, got: %s", query.SQL)
}
if strings.Contains(sqlText, "ALL_TABLES") || strings.Contains(sqlText, "ALL_OBJECTS") {
t.Fatalf("session-user table listing should avoid ALL_* dictionaries, got: %s", query.SQL)
}
if strings.Contains(sqlText, "OWNER =") {
t.Fatalf("session-user table listing should not add owner predicates, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE :1 ESCAPE '\\'") {
t.Fatalf("table listing should push filter predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "TABLE_TYPE IN (:2,:3)") {
t.Fatalf("table listing should push table type predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "ROWNUM <= :4") || !strings.Contains(sqlText, "DBX_RN > :5") {
t.Fatalf("table listing should use rownum pagination, got: %s", query.SQL)
}
if len(query.Args) != 5 {
t.Fatalf("unexpected args: %#v", query.Args)
}
if query.Args[0] != "%U%\\_%R%" || query.Args[1] != "TABLE" || query.Args[2] != "VIEW" || query.Args[3] != 511 || query.Args[4] != 10 {
t.Fatalf("constraints args were not normalized: %#v", query.Args)
}
}
func TestListObjectsSQLUsesSplitDictionaryQuery(t *testing.T) {
sqlText := strings.ToUpper(oracleListObjectsSQL)
if !strings.Contains(sqlText, "ALL_TABLES") || !strings.Contains(sqlText, "ALL_OBJECTS") {
t.Fatalf("object listing should split tables from other objects, got: %s", oracleListObjectsSQL)
}
if !strings.Contains(sqlText, "UNION ALL") {
t.Fatalf("object listing should union object metadata, got: %s", oracleListObjectsSQL)
}
if strings.Contains(sqlText, "ALL_TAB_COMMENTS") {
t.Fatalf("object listing should not load comments during refresh, got: %s", oracleListObjectsSQL)
}
if !strings.Contains(sqlText, "'PACKAGE BODY'") || !strings.Contains(sqlText, "PACKAGE_BODY") {
t.Fatalf("object listing should include package bodies with normalized type, got: %s", oracleListObjectsSQL)
}
}
func TestListObjectsQueryAppliesMetadataConstraints(t *testing.T) {
query := oracleListObjectsQuery("APP", metadataListConstraints{
Filter: "pkg%",
Limit: 25,
ObjectTypes: []string{"FUNCTION", "package"},
})
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE :3 ESCAPE '\\'") {
t.Fatalf("object listing should push filter predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "OBJECT_TYPE IN (:4,:5)") {
t.Fatalf("object listing should push object type predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "ROWNUM <= :6") || !strings.Contains(sqlText, "DBX_RN > :7") {
t.Fatalf("object listing should use rownum pagination, got: %s", query.SQL)
}
if len(query.Args) != 7 {
t.Fatalf("unexpected args: %#v", query.Args)
}
if query.Args[2] != "%P%K%G%\\%%" || query.Args[3] != "FUNCTION" || query.Args[4] != "PACKAGE" || query.Args[5] != 25 || query.Args[6] != 0 {
t.Fatalf("object constraints args were not normalized: %#v", query.Args)
}
}
func TestListSessionUserObjectsQueryUsesUserDictionary(t *testing.T) {
query := oracleListSessionUserObjectsQuery(metadataListConstraints{
Filter: "pkg%",
Limit: 25,
ObjectTypes: []string{"FUNCTION", "package"},
})
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "USER_TABLES") || !strings.Contains(sqlText, "USER_OBJECTS") {
t.Fatalf("session-user object listing should use USER_* dictionaries, got: %s", query.SQL)
}
if strings.Contains(sqlText, "ALL_TABLES") || strings.Contains(sqlText, "ALL_OBJECTS") {
t.Fatalf("session-user object listing should avoid ALL_* dictionaries, got: %s", query.SQL)
}
if strings.Contains(sqlText, "OWNER =") {
t.Fatalf("session-user object listing should not add owner predicates, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE :1 ESCAPE '\\'") {
t.Fatalf("object listing should push filter predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "OBJECT_TYPE IN (:2,:3)") {
t.Fatalf("object listing should push object type predicate, got: %s", query.SQL)
}
if !strings.Contains(sqlText, "ROWNUM <= :4") || !strings.Contains(sqlText, "DBX_RN > :5") {
t.Fatalf("object listing should use rownum pagination, got: %s", query.SQL)
}
if len(query.Args) != 5 {
t.Fatalf("unexpected args: %#v", query.Args)
}
if query.Args[0] != "%P%K%G%\\%%" || query.Args[1] != "FUNCTION" || query.Args[2] != "PACKAGE" || query.Args[3] != 25 || query.Args[4] != 0 {
t.Fatalf("object constraints args were not normalized: %#v", query.Args)
}
}
func TestOracleListTriggersSQLLoadsSourceWithoutLongColumns(t *testing.T) {
sqlText := strings.ToUpper(oracleListTriggersSQL)
if !strings.Contains(sqlText, "FROM ALL_TRIGGERS") || !strings.Contains(sqlText, "LEFT JOIN ALL_SOURCE") {
t.Fatalf("trigger listing should join metadata with line-based source, got: %s", oracleListTriggersSQL)
}
if !strings.Contains(sqlText, "T.DESCRIPTION") || !strings.Contains(sqlText, "S.TEXT") {
t.Fatalf("trigger listing should load the declaration and source text, got: %s", oracleListTriggersSQL)
}
if strings.Contains(sqlText, "TRIGGER_BODY") {
t.Fatalf("trigger listing should avoid Oracle LONG trigger bodies, got: %s", oracleListTriggersSQL)
}
if !strings.Contains(sqlText, "T.OWNER = :1") || !strings.Contains(sqlText, "T.TABLE_NAME = :2") {
t.Fatalf("trigger listing should stay scoped to the selected schema and table, got: %s", oracleListTriggersSQL)
}
}
func TestOracleTriggerBodyStripsDictionaryDeclaration(t *testing.T) {
source := "TRIGGER DBX_TRIGGER_4320_AUDIT\n" +
"AFTER INSERT OR UPDATE OR DELETE ON DBX_TRIGGER_4320\n" +
"FOR EACH ROW\n" +
"DECLARE\n" +
" V_EVENT VARCHAR2(10);\n" +
"BEGIN\n" +
" V_EVENT := CASE WHEN INSERTING THEN 'INSERT' WHEN UPDATING THEN 'UPDATE' ELSE 'DELETE' END;\n" +
"END;\n"
description := "DBX_TRIGGER_4320_AUDIT\n" +
"AFTER INSERT OR UPDATE OR DELETE ON DBX_TRIGGER_4320\n" +
"FOR EACH ROW\n"
body, ok := oracleTriggerBody(source, description)
if !ok {
t.Fatal("expected Oracle trigger source to produce a body")
}
want := "DECLARE\n V_EVENT VARCHAR2(10);\nBEGIN\n V_EVENT := CASE WHEN INSERTING THEN 'INSERT' WHEN UPDATING THEN 'UPDATE' ELSE 'DELETE' END;\nEND;"
if body != want {
t.Fatalf("trigger body = %q, want %q", body, want)
}
}
func TestOracleTriggerBodyFallsBackToVisibleSource(t *testing.T) {
body, ok := oracleTriggerBody("TRIGGER APP.AUDIT\nBEGIN\n NULL;\nEND;\n", "differently formatted declaration")
if !ok {
t.Fatal("expected differently formatted Oracle source to remain visible")
}
if body != "TRIGGER APP.AUDIT\nBEGIN\n NULL;\nEND;" {
t.Fatalf("unexpected fallback source: %q", body)
}
}
func TestOracleFuzzyLikePatternEscapesSpecialCharacters(t *testing.T) {
got := oracleFuzzyLikePattern(`a_%\b`)
want := `%a%\_%\%%\\%b%`
if got != want {
t.Fatalf("oracleFuzzyLikePattern() = %q, want %q", got, want)
}
}
func TestOracleCompletionTablesQuerySearchesAcrossSchemasWithPriority(t *testing.T) {
query := oracleCompletionTablesQuery(completionAssistantRequest{
Database: "ORCL",
Schema: "APP",
ObjectKinds: []string{"table", "view"},
Mask: "dept_d",
GlobalSearch: true,
MatchMode: "prefix",
}, "APP", 201)
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "ALL_OBJECTS") || !strings.Contains(sqlText, "ALL_SYNONYMS") {
t.Fatalf("global completion should include objects and synonyms: %s", query.SQL)
}
if !strings.Contains(sqlText, "S.TABLE_OWNER AS TARGET_OWNER") || !strings.Contains(sqlText, "S.TABLE_NAME AS TARGET_NAME") || !strings.Contains(sqlText, "S.DB_LINK IS NULL") {
t.Fatalf("table completion should return local synonym targets for bounded validation: %s", query.SQL)
}
if strings.Contains(sqlText, "JOIN ALL_OBJECTS TARGET") {
t.Fatalf("Oracle 11g completion must not join full dictionary views before applying the result limit: %s", query.SQL)
}
if !strings.Contains(sqlText, "SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, TARGET_OWNER, TARGET_NAME\nFROM (\nSELECT O.OWNER") {
t.Fatalf("Oracle 11g requires the union to be wrapped before expression-based ordering: %s", query.SQL)
}
if strings.Contains(sqlText, "WHERE UPPER(OBJECT_NAME) LIKE UPPER(:1) ESCAPE '\\' AND OWNER =") {
t.Fatalf("global completion must not restrict results to one owner: %s", query.SQL)
}
if !strings.Contains(sqlText, "WHEN OWNER = :3 THEN 0") || !strings.Contains(sqlText, "WHERE ROWNUM <= :5") {
t.Fatalf("completion should prioritize the current schema and use Oracle 11g rownum limiting: %s", query.SQL)
}
if len(query.Args) != 5 || query.Args[0] != `dept\_d%` || query.Args[1] != `dept\_d%` || query.Args[2] != "APP" || query.Args[3] != "dept_d" || query.Args[4] != 201 {
t.Fatalf("unexpected completion args: %#v", query.Args)
}
}
func TestOracleCompletionSynonymTargetsQueryIsBoundedToCandidates(t *testing.T) {
query := oracleCompletionSynonymTargetsQuery([]oracleCompletionSynonymTarget{{Owner: "DBX_TEST", Name: "DEPT_DICT"}, {Owner: "HR", Name: "EMP_VIEW"}}, []string{"'TABLE'", "'VIEW'"})
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "O.OBJECT_TYPE IN ('TABLE','VIEW')") || !strings.Contains(sqlText, "(O.OWNER = :1 AND O.OBJECT_NAME = :2)") || !strings.Contains(sqlText, "(O.OWNER = :3 AND O.OBJECT_NAME = :4)") {
t.Fatalf("synonym target validation should query only returned targets: %s", query.SQL)
}
wantArgs := []any{"DBX_TEST", "DEPT_DICT", "HR", "EMP_VIEW"}
if !reflect.DeepEqual(query.Args, wantArgs) {
t.Fatalf("unexpected synonym target args: %#v", query.Args)
}
}
func TestOracleCompletionTablesQueryScopesExplicitSchema(t *testing.T) {
query := oracleCompletionTablesQuery(completionAssistantRequest{
Schema: "APP",
ParentSchema: "HR",
ObjectKinds: []string{"table"},
Mask: "EMP",
}, "APP", 50)
if !strings.Contains(strings.ToUpper(query.SQL), "AND O.OWNER = :2") || !strings.Contains(strings.ToUpper(query.SQL), "AND S.OWNER = :4") {
t.Fatalf("explicit schema completion should restrict owner: %s", query.SQL)
}
if len(query.Args) != 7 || query.Args[1] != "HR" || query.Args[3] != "HR" || query.Args[4] != "APP" {
t.Fatalf("unexpected scoped completion args: %#v", query.Args)
}
}
func TestOracleCompletionRoutinesQueryUsesPublicPackageMetadata(t *testing.T) {
query := oracleCompletionRoutinesQuery(completionAssistantRequest{
Schema: "HR",
ParentSchema: "HR",
ParentName: "PAYROLL",
ObjectKinds: []string{"routine"},
Mask: "CALC",
}, "HR", 200)
sqlText := strings.ToUpper(query.SQL)
if !strings.Contains(sqlText, "ALL_PROCEDURES") || !strings.Contains(sqlText, "ALL_ARGUMENTS") {
t.Fatalf("package completion should use callable procedure metadata: %s", query.SQL)
}
if strings.Contains(sqlText, "ALL_SOURCE") || strings.Contains(sqlText, "PACKAGE BODY") {
t.Fatalf("package completion must not expose private package body source: %s", query.SQL)
}
if !strings.Contains(sqlText, "P.OBJECT_NAME = :1") || !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE UPPER(:2)") || !strings.Contains(sqlText, "AND OWNER = :3") {
t.Fatalf("package completion should scope package and owner: %s", query.SQL)
}
if len(query.Args) != 6 || query.Args[0] != "PAYROLL" || query.Args[1] != "CALC%" || query.Args[2] != "HR" {
t.Fatalf("unexpected package completion args: %#v", query.Args)
}
}
func TestOracleCompletionLikePatternSupportsPrefixAndContains(t *testing.T) {
if got := oracleCompletionLikePattern(`A_%`, "prefix"); got != `A\_\%%` {
t.Fatalf("prefix pattern = %q", got)
}
if got := oracleCompletionLikePattern("DEPT", "contains"); got != "%DEPT%" {
t.Fatalf("contains pattern = %q", got)
}
}
func TestIsOraclePGALimitError(t *testing.T) {
if !isOraclePGALimitError(errors.New("ORA-04036: PGA memory used by the instance exceeds PGA_AGGREGATE_LIMIT")) {
t.Fatal("expected ORA-04036 to be detected")
}
if isOraclePGALimitError(errors.New("ORA-00942: table or view does not exist")) {
t.Fatal("unexpected ORA-00942 match")
}
}
func TestRewriteOracleXMLTypeSelectStar(t *testing.T) {
sqlText, err := rewriteOracleXMLTypeSelectSQL(
`SELECT * FROM TEST_LOBS`,
fakeOracleColumnLoader([]oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "XML_CONTENT", DataType: "XMLTYPE"},
{Name: "TEST_NAME", DataType: "VARCHAR2"},
}),
)
if err != nil {
t.Fatal(err)
}
want := `SELECT "ID", XMLSERIALIZE(CONTENT "XML_CONTENT" AS CLOB) AS "XML_CONTENT", "TEST_NAME" FROM TEST_LOBS`
if sqlText != want {
t.Fatalf("rewriteOracleXMLTypeSelectSQL() = %s, want %s", sqlText, want)
}
}
func TestRewriteOracleXMLTypeExplicitColumn(t *testing.T) {
sqlText, err := rewriteOracleXMLTypeSelectSQL(
`SELECT t.ID, t.XML_CONTENT AS xml_doc FROM TEST_LOBS t WHERE t.ID = 1`,
fakeOracleColumnLoader([]oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "XML_CONTENT", DataType: "SYS.XMLTYPE"},
}),
)
if err != nil {
t.Fatal(err)
}
want := `SELECT t.ID, XMLSERIALIZE(CONTENT t."XML_CONTENT" AS CLOB) AS xml_doc FROM TEST_LOBS t WHERE t.ID = 1`
if sqlText != want {
t.Fatalf("rewriteOracleXMLTypeSelectSQL() = %s, want %s", sqlText, want)
}
}
func TestRewriteOracleXMLTypeNestedRownumQuery(t *testing.T) {
sqlText, err := rewriteOracleXMLTypeSelectSQL(
`SELECT * FROM (SELECT "ID", "XML_CONTENT" FROM "DBX"."TEST_LOBS") WHERE ROWNUM <= 100`,
fakeOracleColumnLoader([]oracleColumnMeta{
{Name: "ID", DataType: "NUMBER"},
{Name: "XML_CONTENT", DataType: "XMLTYPE"},
}),
)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(sqlText, `XMLSERIALIZE(CONTENT "XML_CONTENT" AS CLOB) AS "XML_CONTENT"`) {
t.Fatalf("expected nested XMLTYPE column to be serialized, got: %s", sqlText)
}
}
func TestRewriteOracleXMLTypeSkipsJoins(t *testing.T) {
called := false
sqlText, err := rewriteOracleXMLTypeSelectSQL(
`SELECT * FROM TEST_LOBS l JOIN OTHER_TABLE o ON o.ID = l.ID`,
func(schema, table string) ([]oracleColumnMeta, error) {
called = true
return nil, nil
},
)
if err != nil {
t.Fatal(err)
}
if called {
t.Fatal("join query should not load table metadata")
}
if sqlText != `SELECT * FROM TEST_LOBS l JOIN OTHER_TABLE o ON o.ID = l.ID` {
t.Fatalf("join query should not be rewritten, got: %s", sqlText)
}
}
func TestOracleColumnTypeNamesContainXMLType(t *testing.T) {
tests := []struct {
name string
typeNames []string
want bool
}{
{name: "plain xmltype", typeNames: []string{"NUMBER", "XMLTYPE"}, want: true},
{name: "qualified xmltype", typeNames: []string{"SYS.XMLTYPE"}, want: true},
{name: "case and spaces", typeNames: []string{" varchar2 ", "sys.xmltype"}, want: true},
{name: "ordinary columns", typeNames: []string{"NUMBER", "VARCHAR2", "DATE"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := oracleColumnTypeNamesContainXMLType(tt.typeNames); got != tt.want {
t.Fatalf("oracleColumnTypeNamesContainXMLType(%v) = %v, want %v", tt.typeNames, got, tt.want)
}
})
}
}
func fakeOracleColumnLoader(columns []oracleColumnMeta) oracleColumnMetaLoader {
return func(schema, table string) ([]oracleColumnMeta, error) {
if strings.ToUpper(table) != "TEST_LOBS" {
return nil, nil
}
return columns, nil
}
}
func contains(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
// -- fake drivers for timeout tests --
func init() {
sql.Register("oracle-test-dml", &oracleDMLDriver{})
sql.Register("oracle-test-fast", &oracleFastDriver{})
}
// oracleDMLDriver: ExecContext blocks until ctx.Done, simulating a long-running DML.
type oracleDMLDriver struct{}
func (d *oracleDMLDriver) Open(name string) (driver.Conn, error) {
return &oracleDMLConn{}, nil
}
type oracleDMLConn struct{}
func (c *oracleDMLConn) Prepare(query string) (driver.Stmt, error) {
return nil, errors.New("use ExecContext directly")
}
func (c *oracleDMLConn) Close() error { return nil }
func (c *oracleDMLConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
var _ driver.ExecerContext = (*oracleDMLConn)(nil)
func (c *oracleDMLConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
<-ctx.Done()
return nil, ctx.Err()
}
// oracleFastDriver: returns rows quickly for cursor survival tests.
type oracleFastDriver struct{}
func (d *oracleFastDriver) Open(name string) (driver.Conn, error) {
return &oracleFastConn{}, nil
}
type oracleFastConn struct{}
func (c *oracleFastConn) Prepare(query string) (driver.Stmt, error) {
return &oracleFastStmt{}, nil
}
func (c *oracleFastConn) Close() error { return nil }
func (c *oracleFastConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
type oracleFastStmt struct{}
func (s *oracleFastStmt) Close() error { return nil }
func (s *oracleFastStmt) NumInput() int { return -1 }
func (s *oracleFastStmt) Exec(args []driver.Value) (driver.Result, error) {
return driver.ResultNoRows, nil
}
func (s *oracleFastStmt) Query(args []driver.Value) (driver.Rows, error) {
return &oracleFastRows{}, nil
}
type oracleFastRows struct {
pos int
closed bool
}
func (r *oracleFastRows) Columns() []string { return []string{"id"} }
func (r *oracleFastRows) Close() error { r.closed = true; return nil }
func (r *oracleFastRows) Next(dest []driver.Value) error {
if r.pos >= 3 || r.closed {
return io.EOF
}
dest[0] = int64(r.pos + 1)
r.pos++
return nil
}
// -- timeout tests --
func TestOracleDMLCancelInterruptsExecContext(t *testing.T) {
s := newServer()
db, err := sql.Open("oracle-test-dml", "dsn")
if err != nil {
t.Fatal(err)
}
s.db = db
errCh := make(chan error, 1)
go func() {
_, execErr := s.executeQuery(queryOptions{
SQL: "UPDATE test SET x = 1",
TimeoutSecs: 0,
})
errCh <- execErr
}()
// Give the goroutine time to enter ExecContext and block.
time.Sleep(200 * time.Millisecond)
s.cancelActiveQuery()
select {
case execErr := <-errCh:
if execErr == nil {
t.Fatal("expected non-nil error after DML cancel")
}
case <-time.After(3 * time.Second):
t.Fatal("executeQuery did not return after cancelActiveQuery")
}
}
func TestOracleCursorSurvivesDeadlineWindow(t *testing.T) {
s := newServer()
db, err := sql.Open("oracle-test-fast", "dsn")
if err != nil {
t.Fatal(err)
}
s.db = db
rows, err := s.queryRowsWithTimeout("SELECT id FROM test", nil, 1)
if err != nil {
t.Fatalf("queryRowsWithTimeout failed: %v", err)
}
defer s.closeRows(rows)
s.activeCancelMu.Lock()
timerStopped := s.activeTimer == nil
s.activeCancelMu.Unlock()
if !timerStopped {
t.Fatal("timer should be stopped after QueryContext returns")
}
time.Sleep(1200 * time.Millisecond)
// Read all rows to verify cursor survived the deadline window.
cols, _ := rows.Columns()
for range cols {
// placeholder
}
rowCount := 0
for rows.Next() {
rowCount++
}
if err := rows.Err(); err != nil {
t.Fatalf("cursor was killed by deadline: %v", err)
}
if rowCount != 3 {
t.Fatalf("expected 3 rows, got %d", rowCount)
}
}