package main import ( "context" "database/sql" "database/sql/driver" "encoding/json" "errors" "io" "math" "net/url" "os" "reflect" "strings" "testing" "time" go_ora "github.com/sijms/go-ora/v2" "github.com/sijms/go-ora/v2/configurations" ) 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 TestQueryResultsMarshalNonFiniteFloatsAsStrings(t *testing.T) { result := queryResult{Rows: [][]any{{math.NaN(), math.Inf(1), math.Inf(-1), 1234.56}}} data, err := json.Marshal(result) if err != nil { t.Fatalf("non-finite Oracle floats must remain JSON-safe: %v", err) } if !strings.Contains(string(data), `[["NaN","+Inf","-Inf",1234.56]]`) { t.Fatalf("unexpected query result JSON: %s", data) } if !math.IsNaN(result.Rows[0][0].(float64)) { t.Fatalf("marshaling must not mutate the original rows: %#v", result.Rows) } data, err = json.Marshal(queryPageResult{Rows: [][]any{{math.NaN()}}}) if err != nil { t.Fatalf("paged non-finite Oracle floats must remain JSON-safe: %v", err) } if !strings.Contains(string(data), `[["NaN"]]`) { t.Fatalf("unexpected query page JSON: %s", data) } } 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 TestExecuteOracleSelectRetriesXMLTypeDecodeFailures(t *testing.T) { originalSQL := `SELECT * FROM (SELECT * FROM "DBX"."TEST_LOBS") WHERE ROWNUM <= 100` rewrittenSQL := `SELECT * FROM (SELECT "ID", XMLSERIALIZE(CONTENT "XML_CONTENT" AS CLOB) AS "XML_CONTENT" FROM "DBX"."TEST_LOBS") WHERE ROWNUM <= 100` calls := []string{} result, err := executeOracleSelectWithXMLTypeRetry( originalSQL, func(sqlText string) (queryResult, error) { calls = append(calls, sqlText) if sqlText == originalSQL { return queryResult{}, errors.New("abnormal data representation for date") } return queryResult{Columns: []string{"ID", "XML_CONTENT"}, Rows: [][]any{{"1", ""}}}, nil }, func(sqlText string) (string, error) { if sqlText != originalSQL { t.Fatalf("rewrite input = %q, want original SQL", sqlText) } return rewrittenSQL, nil }, ) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(calls, []string{originalSQL, rewrittenSQL}) { t.Fatalf("execute calls = %#v, want original and rewritten SQL", calls) } if len(result.Rows) != 1 || result.Rows[0][1] != "" { t.Fatalf("unexpected retry result: %#v", result) } } func TestExecuteOracleSelectDoesNotRewriteSuccessfulQueries(t *testing.T) { calls := 0 rewriteCalled := false want := queryResult{Columns: []string{"ID"}, Rows: [][]any{{"1"}}} result, err := executeOracleSelectWithXMLTypeRetry( `SELECT ID FROM TEST_TABLE`, func(string) (queryResult, error) { calls++ return want, nil }, func(sqlText string) (string, error) { rewriteCalled = true return sqlText, nil }, ) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(result, want) { t.Fatalf("result = %#v, want %#v", result, want) } if calls != 1 || rewriteCalled { t.Fatalf("successful query should not rewrite: calls=%d rewriteCalled=%t", calls, rewriteCalled) } } func TestExecuteOracleSelectDoesNotRetryOrdinaryErrors(t *testing.T) { calls := 0 rewriteCalled := false originalErr := errors.New("ORA-00942: table or view does not exist") _, err := executeOracleSelectWithXMLTypeRetry( `SELECT * FROM MISSING_TABLE`, func(string) (queryResult, error) { calls++ return queryResult{}, originalErr }, func(sqlText string) (string, error) { rewriteCalled = true return sqlText, nil }, ) if !errors.Is(err, originalErr) { t.Fatalf("error = %v, want original error", err) } if calls != 1 || rewriteCalled { t.Fatalf("ordinary error should not retry: calls=%d rewriteCalled=%t", calls, rewriteCalled) } } func TestExecuteOracleSelectKeepsDecodeErrorWhenNoXMLTypeRewriteApplies(t *testing.T) { calls := 0 originalErr := errors.New("TTC error: received code 36 during response reading") sqlText := `SELECT * FROM TEST_DATES` _, err := executeOracleSelectWithXMLTypeRetry( sqlText, func(string) (queryResult, error) { calls++ return queryResult{}, originalErr }, func(input string) (string, error) { return input, nil }, ) if !errors.Is(err, originalErr) { t.Fatalf("error = %v, want original error", err) } if calls != 1 { t.Fatalf("unchanged SQL should not retry, got %d executions", calls) } } 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 TestGetTableDDLPreservesQuotedObjectName(t *testing.T) { const schema = "ZTZS_ERP2" const table = "ZGJ_FlowSealTemplate" const ddl = `CREATE TABLE "ZTZS_ERP2"."ZGJ_FlowSealTemplate" ("FlowId" NUMBER)` db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_OBJECTS", args: []driver.Value{schema, table}, rows: [][]driver.Value{{"TABLE"}}, }, { queryContains: "DBMS_METADATA.GET_DDL", args: []driver.Value{"TABLE", table, schema}, rows: [][]driver.Value{{ddl}}, }, }) s := newServer() s.db = db got, err := s.getTableDDL(schema, table, "") if err != nil { t.Fatal(err) } if got != ddl { t.Fatalf("getTableDDL() = %q, want %q", got, ddl) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestGetTableDDLUppercaseObjectDoesNotAddFallbackQuery(t *testing.T) { const schema = "ZTZS_ERP2" const table = "ORDERS" const ddl = `CREATE TABLE "ZTZS_ERP2"."ORDERS" ("ID" NUMBER)` db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_OBJECTS", args: []driver.Value{schema, table}, rows: [][]driver.Value{{"TABLE"}}, }, { queryContains: "DBMS_METADATA.GET_DDL", args: []driver.Value{"TABLE", table, schema}, rows: [][]driver.Value{{ddl}}, }, }) s := newServer() s.db = db got, err := s.getTableDDL(schema, table, "") if err != nil { t.Fatal(err) } if got != ddl { t.Fatalf("getTableDDL() = %q, want %q", got, ddl) } if scripted.next != 2 { t.Fatalf("uppercase object should use two queries, got %d", scripted.next) } } func TestGetTableDDLFallsBackToUppercaseAfterExactMiss(t *testing.T) { const schema = "ZTZS_ERP2" const ddl = `CREATE TABLE "ZTZS_ERP2"."ORDERS" ("ID" NUMBER)` db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_OBJECTS", args: []driver.Value{schema, "orders"}, rows: nil, }, { queryContains: "FROM ALL_OBJECTS", args: []driver.Value{schema, "ORDERS"}, rows: [][]driver.Value{{"TABLE"}}, }, { queryContains: "DBMS_METADATA.GET_DDL", args: []driver.Value{"TABLE", "ORDERS", schema}, rows: [][]driver.Value{{ddl}}, }, }) s := newServer() s.db = db got, err := s.getTableDDL(schema, "orders", "") if err != nil { t.Fatal(err) } if got != ddl { t.Fatalf("getTableDDL() = %q, want %q", got, ddl) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestGetTableDDLFallbackPreservesQuotedColumnNames(t *testing.T) { const schema = "ZTZS_ERP2" const table = "ZGJ_FlowSealTemplate" db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_OBJECTS", args: []driver.Value{schema, table}, rows: [][]driver.Value{{"TABLE"}}, }, { queryContains: "DBMS_METADATA.GET_DDL", args: []driver.Value{"TABLE", table, schema}, err: errors.New("ORA-31603: object not found"), }, { queryContains: "FROM ALL_TAB_COLUMNS", args: []driver.Value{schema, table}, columns: []string{ "COLUMN_NAME", "DATA_TYPE", "NULLABLE", "DATA_DEFAULT", "IS_PRIMARY_KEY", "COMMENTS", "DATA_PRECISION", "DATA_SCALE", "CHAR_LENGTH", }, rows: [][]driver.Value{{"FlowId", "VARCHAR2", "N", nil, int64(1), nil, nil, nil, int64(50)}}, }, }) s := newServer() s.db = db got, err := s.getTableDDL(schema, table, "") if err != nil { t.Fatal(err) } want := `CREATE TABLE "ZTZS_ERP2"."ZGJ_FlowSealTemplate" ( "FlowId" VARCHAR2(50) NOT NULL, PRIMARY KEY ("FlowId") )` if got != want { t.Fatalf("getTableDDL() = %q, want %q", got, want) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestGetTableDDLAppendsIndexesTriggersAndComments(t *testing.T) { const schema = "HR" const table = "ORDERS" const tableDDL = `CREATE TABLE "HR"."ORDERS" ("ID" NUMBER DEFAULT 42, PRIMARY KEY ("ID"))` db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "DBMS_METADATA.GET_DDL(:1, :2, :3)", args: []driver.Value{"TABLE", table, schema}, rows: [][]driver.Value{{tableDDL}}, }, { queryContains: "FROM ALL_INDEXES", args: []driver.Value{schema, table, schema, table}, rows: [][]driver.Value{{`CREATE INDEX "HR"."IDX_ORDERS_STATUS" ON "HR"."ORDERS" ("STATUS")`}}, }, { queryContains: "FROM ALL_TRIGGERS", args: []driver.Value{schema, table}, rows: [][]driver.Value{{`CREATE OR REPLACE TRIGGER "HR"."TRG_ORDERS" BEFORE INSERT ON "HR"."ORDERS" BEGIN NULL; END;`}}, }, { queryContains: "FROM ALL_TAB_COMMENTS", args: []driver.Value{schema, table}, rows: [][]driver.Value{{"Owner's orders"}}, }, { queryContains: "FROM ALL_COL_COMMENTS", args: []driver.Value{schema, table}, columns: []string{"COLUMN_NAME", "COMMENTS"}, rows: [][]driver.Value{{"STATUS", "Order's state"}}, }, }) s := newServer() s.db = db got, err := s.getTableDDL(schema, table, "TABLE") if err != nil { t.Fatal(err) } for _, fragment := range []string{ tableDDL, `CREATE INDEX "HR"."IDX_ORDERS_STATUS"`, `CREATE OR REPLACE TRIGGER "HR"."TRG_ORDERS"`, `COMMENT ON TABLE "HR"."ORDERS" IS 'Owner''s orders';`, `COMMENT ON COLUMN "HR"."ORDERS"."STATUS" IS 'Order''s state';`, } { if !strings.Contains(got, fragment) { t.Fatalf("getTableDDL() missing %q:\n%s", fragment, got) } } if !strings.Contains(got, tableDDL+";\n\nCREATE INDEX") { t.Fatalf("base table DDL should be terminated before dependent DDL:\n%s", got) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestListIndexesSeparatesQuotedCloneTableNamesByCase(t *testing.T) { const schema = "HR" db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_INDEXES", args: []driver.Value{schema, "ORDERS_COPY"}, columns: []string{"INDEX_NAME", "COLUMN_NAME", "UNIQUENESS", "IS_PRIMARY", "INDEX_TYPE", "COLUMN_POSITION"}, rows: [][]driver.Value{{"ORDERS_COPY_IDX1", "STATUS", "NONUNIQUE", int64(0), "NORMAL", int64(1)}}, }, { queryContains: "FROM ALL_INDEXES", args: []driver.Value{schema, "ORDERS_copy"}, columns: []string{"INDEX_NAME", "COLUMN_NAME", "UNIQUENESS", "IS_PRIMARY", "INDEX_TYPE", "COLUMN_POSITION"}, rows: [][]driver.Value{{"ORDERS_copy_IDX1", "STATUS", "NONUNIQUE", int64(0), "NORMAL", int64(1)}}, }, }) s := newServer() s.db = db uppercaseIndexes, err := s.listIndexes(schema, "ORDERS_COPY") if err != nil { t.Fatal(err) } mixedCaseIndexes, err := s.listIndexes(schema, "ORDERS_copy") if err != nil { t.Fatal(err) } if len(uppercaseIndexes) != 1 || uppercaseIndexes[0].Name != "ORDERS_COPY_IDX1" { t.Fatalf("listIndexes(ORDERS_COPY) = %#v, want uppercase table index", uppercaseIndexes) } if len(mixedCaseIndexes) != 1 || mixedCaseIndexes[0].Name != "ORDERS_copy_IDX1" || !reflect.DeepEqual(mixedCaseIndexes[0].Columns, []string{"STATUS"}) { t.Fatalf("listIndexes(ORDERS_copy) = %#v, want quoted clone index", mixedCaseIndexes) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestGetTableCommentPreservesQuotedObjectName(t *testing.T) { const schema = "HR" const table = "OrderDetails" const comment = "Quoted table comment" db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_TAB_COMMENTS", args: []driver.Value{schema, table}, rows: [][]driver.Value{{comment}}, }, }) s := newServer() s.db = db got, err := s.getTableComment(schema, table) if err != nil { t.Fatal(err) } if got == nil || *got != comment { t.Fatalf("getTableComment() = %#v, want %q", got, comment) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestGetTableCommentFallsBackToUppercase(t *testing.T) { const schema = "HR" const comment = "Orders comment" db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_TAB_COMMENTS", args: []driver.Value{schema, "orders"}, rows: nil, }, { queryContains: "FROM ALL_TAB_COMMENTS", args: []driver.Value{schema, "ORDERS"}, rows: [][]driver.Value{{comment}}, }, }) s := newServer() s.db = db got, err := s.getTableComment(schema, "orders") if err != nil { t.Fatal(err) } if got == nil || *got != comment { t.Fatalf("getTableComment() = %#v, want %q", got, comment) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestListForeignKeysIncludesReferencedSchemaAndDeleteRule(t *testing.T) { const schema = "HR" const table = "ORDERS" db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_CONSTRAINTS ac", args: []driver.Value{schema, table}, columns: []string{"CONSTRAINT_NAME", "COLUMN_NAME", "REF_SCHEMA", "REF_TABLE", "REF_COLUMN", "DELETE_RULE"}, rows: [][]driver.Value{{"FK_ORDERS_CUSTOMER", "CUSTOMER_ID", "CRM", "CUSTOMERS", "ID", "CASCADE"}}, }, }) s := newServer() s.db = db got, err := s.listForeignKeys(schema, table) if err != nil { t.Fatal(err) } want := []foreignKeyInfo{{ Name: "FK_ORDERS_CUSTOMER", Column: "CUSTOMER_ID", RefSchema: "CRM", RefTable: "CUSTOMERS", RefColumn: "ID", OnDelete: "CASCADE", }} if !reflect.DeepEqual(got, want) { t.Fatalf("listForeignKeys() = %#v, want %#v", got, want) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestListForeignKeysAndTriggersPreserveQuotedCloneTableName(t *testing.T) { const schema = "HR" const table = "ORDERS_copy" db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "FROM ALL_CONSTRAINTS ac", args: []driver.Value{schema, table}, columns: []string{"CONSTRAINT_NAME", "COLUMN_NAME", "REF_SCHEMA", "REF_TABLE", "REF_COLUMN", "DELETE_RULE"}, rows: [][]driver.Value{{"ORDERS_copy_FK1", "CUSTOMER_ID", "CRM", "CUSTOMERS", "ID", "CASCADE"}}, }, { queryContains: "FROM ALL_TRIGGERS", args: []driver.Value{schema, table}, columns: []string{"TRIGGER_NAME", "TRIGGERING_EVENT", "TRIGGER_TYPE", "DESCRIPTION", "LINE", "TEXT"}, rows: [][]driver.Value{{"ORDERS_copy_TRG1", "INSERT", "BEFORE EACH ROW", nil, nil, nil}}, }, }) s := newServer() s.db = db foreignKeys, err := s.listForeignKeys(schema, table) if err != nil { t.Fatal(err) } triggers, err := s.listTriggers(schema, table) if err != nil { t.Fatal(err) } if len(foreignKeys) != 1 || foreignKeys[0].Name != "ORDERS_copy_FK1" { t.Fatalf("listForeignKeys(%q) = %#v, want quoted clone foreign key", table, foreignKeys) } if len(triggers) != 1 || triggers[0].Name != "ORDERS_copy_TRG1" { t.Fatalf("listTriggers(%q) = %#v, want quoted clone trigger", table, triggers) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } 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 TestOracleExplainTargetSchemaIgnoresSysDBAServicePrefix(t *testing.T) { if got := oracleExplainTargetSchema("ORCLPDB1", "", "SYSDBA:ORCLPDB1"); got != "" { t.Fatalf("oracleExplainTargetSchema() = %q, want no schema switch", got) } } func TestOracleExplainTargetSchemaKeepsSelectedSchema(t *testing.T) { if got := oracleExplainTargetSchema("APP", "", "SYSDBA:ORCLPDB1"); got != "APP" { t.Fatalf("oracleExplainTargetSchema() = %q, want APP", got) } if got := oracleExplainTargetSchema("ORCLPDB1", "REPORTING", "SYSDBA:ORCLPDB1"); got != "REPORTING" { t.Fatalf("oracleExplainTargetSchema() = %q, want REPORTING", got) } } 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 TestParseOracleMajorVersion(t *testing.T) { tests := []struct { version string major int ok bool }{ {version: "10.2.0.4.0", major: 10, ok: true}, {version: "11.2.0.4.0 Production", major: 11, ok: true}, {version: "Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - 64bit Production", major: 10, ok: true}, {version: "19.0.0.0.0", major: 19, ok: true}, {version: "", ok: false}, {version: "Oracle Database 10g", ok: false}, } for _, tt := range tests { t.Run(tt.version, func(t *testing.T) { major, ok := parseOracleMajorVersion(tt.version) if major != tt.major || ok != tt.ok { t.Fatalf("parseOracleMajorVersion(%q) = (%d, %t), want (%d, %t)", tt.version, major, ok, tt.major, tt.ok) } }) } } 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{ { queryContains: "PRODUCT_COMPONENT_VERSION", args: []driver.Value{}, rows: [][]driver.Value{{"10.2.0.4.0"}}, }, }) major, ok := oracleServerMajorVersion(db, time.Second) if !ok || major != 10 { t.Fatalf("oracleServerMajorVersion() = (%d, %t), want (10, true)", major, ok) } if scripted.next != 1 { t.Fatalf("expected one version query, got %d", scripted.next) } } func TestOracleServerMajorVersionFallsBackToVersionBanner(t *testing.T) { db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "PRODUCT_COMPONENT_VERSION", args: []driver.Value{}, err: errors.New("view unavailable"), }, { queryContains: "V$VERSION", args: []driver.Value{}, rows: [][]driver.Value{{ "Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production", }}, }, }) major, ok := oracleServerMajorVersion(db, time.Second) if !ok || major != 11 { t.Fatalf("oracleServerMajorVersion() = (%d, %t), want (11, true)", major, ok) } if scripted.next != 2 { t.Fatalf("expected both version queries, got %d", scripted.next) } } func TestWithOracleLOBFetchPostUsesURLParamsForGeneratedDSN(t *testing.T) { params := withOracleLOBFetchPost(connectParams{ Host: "db.example.com", Port: 1521, Database: "ORCL", Username: "scott", Password: "tiger", URLParams: "CHARSET=ZHS16GBK", }) values, err := url.ParseQuery(params.URLParams) if err != nil { t.Fatal(err) } if values.Get("LOB FETCH") != "POST" || values.Get("CHARSET") != "ZHS16GBK" { t.Fatalf("legacy LOB mode should preserve URL parameters, got: %s", params.URLParams) } config, err := configurations.ParseConfig(buildDSN(params)) if err != nil { t.Fatal(err) } if config.Lob != configurations.STREAM { t.Fatalf("generated DSN should enable streamed LOB reads, got: %s", buildDSN(params)) } } func TestWithOracleLOBFetchPostUpdatesRawOracleURL(t *testing.T) { params := withOracleLOBFetchPost(connectParams{ ConnectionString: "oracle://scott:tiger@db.example.com:1521/ORCL?CHARSET=ZHS16GBK", }) parsed, err := url.Parse(params.ConnectionString) if err != nil { t.Fatal(err) } if parsed.Query().Get("LOB FETCH") != "POST" || parsed.Query().Get("CHARSET") != "ZHS16GBK" { t.Fatalf("raw Oracle URL should preserve query parameters, got: %s", params.ConnectionString) } config, err := configurations.ParseConfig(params.ConnectionString) if err != nil { t.Fatal(err) } if config.Lob != configurations.STREAM { t.Fatalf("raw Oracle URL should enable streamed LOB reads, got: %s", params.ConnectionString) } } func TestHasOracleLOBFetchOptionHonorsExplicitModes(t *testing.T) { tests := []connectParams{ {URLParams: "lob+fetch=inline"}, {URLParams: "LOB%20FETCH=POST"}, {ConnectionString: "oracle://scott:tiger@db.example.com:1521/ORCL?lob+fetch=stream"}, } for _, params := range tests { if !hasOracleLOBFetchOption(params) { t.Fatalf("explicit LOB fetch mode should be detected: %+v", params) } } if hasOracleLOBFetchOption(connectParams{URLParams: "CHARSET=ZHS16GBK"}) { t.Fatal("unrelated URL parameters should not be treated as an explicit LOB fetch mode") } } func TestShouldUseLegacyOracleLOBFetchOnlyForLegacyServers(t *testing.T) { params := connectParams{URLParams: "CHARSET=ZHS16GBK"} if !shouldUseLegacyOracleLOBFetch(params, 10, true) { t.Fatal("Oracle 10g should use streamed LOB reads") } 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") } if shouldUseLegacyOracleLOBFetch(connectParams{URLParams: "LOB+FETCH=INLINE"}, 10, true) { t.Fatal("an explicit user LOB mode should not be overridden") } } 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" 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 TestOpenDBUsesIndependentOracleDrivers(t *testing.T) { params := connectParams{ Host: "127.0.0.1", Port: 1521, Database: "ORCL", Username: "dbx", Password: "secret", } first, err := openDB(params) if err != nil { t.Fatal(err) } defer first.Close() second, err := openDB(params) if err != nil { t.Fatal(err) } defer second.Close() if first.Driver() == second.Driver() { t.Fatal("Oracle connections must not share the driver's cached charset converters") } } 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 := oracleCompletionPackageRoutinesQuery(completionAssistantRequest{ Schema: "HR", ParentSchema: "HR", ParentName: "PAYROLL", ObjectKinds: []string{"routine"}, Mask: "CALC", }, "HR") 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.OWNER = :1") || !strings.Contains(sqlText, "P.OBJECT_NAME = :2") || !strings.Contains(sqlText, "UPPER(P.PROCEDURE_NAME) LIKE UPPER(:3)") { t.Fatalf("package completion should scope package and owner: %s", query.SQL) } if !strings.Contains(sqlText, "A.OBJECT_ID = P.OBJECT_ID") || !strings.Contains(sqlText, "A.SUBPROGRAM_ID = P.SUBPROGRAM_ID") || !strings.Contains(sqlText, "A.DATA_LEVEL = 0") { t.Fatalf("package completion should join complete top-level argument metadata: %s", query.SQL) } if strings.Contains(sqlText, "ROWNUM") { t.Fatalf("package completion must not truncate argument rows before overload grouping: %s", query.SQL) } wantArgs := []any{"HR", "PAYROLL", "CALC%"} if !reflect.DeepEqual(query.Args, wantArgs) { t.Fatalf("unexpected package completion args: %#v", query.Args) } } func TestOracleCompletionPackageCandidatesPreserveOverloadsAndReturnTypes(t *testing.T) { rows := []oraclePackageRoutineRow{ { owner: "HR", parentName: "PAYROLL", name: "CALCULATE", objectID: 42, subprogramID: 1, position: sql.NullInt64{Int64: 1, Valid: true}, sequence: sql.NullInt64{Int64: 1, Valid: true}, argumentName: sql.NullString{String: "P_VALUE", Valid: true}, inOut: sql.NullString{String: "IN", Valid: true}, dataType: sql.NullString{String: "NUMBER", Valid: true}, }, { owner: "HR", parentName: "PAYROLL", name: "CALCULATE", objectID: 42, subprogramID: 2, position: sql.NullInt64{Int64: 1, Valid: true}, sequence: sql.NullInt64{Int64: 1, Valid: true}, argumentName: sql.NullString{String: "P_VALUE", Valid: true}, inOut: sql.NullString{String: "IN", Valid: true}, dataType: sql.NullString{String: "VARCHAR2", Valid: true}, }, { owner: "HR", parentName: "PAYROLL", name: "ITEM_COUNT", objectID: 42, subprogramID: 3, position: sql.NullInt64{Int64: 0, Valid: true}, sequence: sql.NullInt64{Int64: 1, Valid: true}, dataType: sql.NullString{String: "NUMBER", Valid: true}, }, { owner: "HR", parentName: "PAYROLL", name: "ITEM_COUNT", objectID: 42, subprogramID: 3, position: sql.NullInt64{Int64: 1, Valid: true}, sequence: sql.NullInt64{Int64: 2, Valid: true}, argumentName: sql.NullString{String: "P_ACTIVE", Valid: true}, inOut: sql.NullString{String: "IN/OUT", Valid: true}, dataType: sql.NullString{String: "NUMBER", Valid: true}, }, } response := oracleCompletionPackageCandidates(rows, "XE", 10) if response.Incomplete { t.Fatal("complete package metadata should not be marked incomplete") } if len(response.Candidates) != 3 { t.Fatalf("overloads should remain distinct candidates: %#v", response.Candidates) } want := []struct { name string kind string signature string returnType string }{ {name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN NUMBER"}, {name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN VARCHAR2"}, {name: "ITEM_COUNT", kind: "function", signature: "P_ACTIVE IN OUT NUMBER", returnType: "NUMBER"}, } for index, expected := range want { candidate := response.Candidates[index] if candidate.Name != expected.name || candidate.Kind != expected.kind || candidate.Signature == nil || *candidate.Signature != expected.signature { t.Fatalf("candidate %d = %#v, want name=%q kind=%q signature=%q", index, candidate, expected.name, expected.kind, expected.signature) } if expected.returnType == "" { if candidate.DataType != nil { t.Fatalf("candidate %d return type = %#v, want nil", index, candidate.DataType) } } else if candidate.DataType == nil || *candidate.DataType != expected.returnType { t.Fatalf("candidate %d return type = %#v, want %q", index, candidate.DataType, expected.returnType) } if candidate.ParentSchema == nil || *candidate.ParentSchema != "HR" || candidate.ParentName == nil || *candidate.ParentName != "PAYROLL" { t.Fatalf("candidate %d lost package identity: %#v", index, candidate) } } limited := oracleCompletionPackageCandidates(rows, "XE", 2) if !limited.Incomplete || len(limited.Candidates) != 2 { t.Fatalf("member limit should apply after overload grouping: %#v", limited) } } 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 TestGetObjectSourceUsesOriginalViewNameWithDBMSMetadata(t *testing.T) { for _, viewName := range []string{"vEnginWJZ", "V_ENGINE_WJZ"} { t.Run(viewName, func(t *testing.T) { ddl := `CREATE OR REPLACE FORCE VIEW "ZTZS_ERP2"."` + viewName + `" AS SELECT source_id FROM "ZTZS_ERP2"."SOURCE_TABLE"` db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "DBMS_METADATA.GET_DDL('VIEW'", args: []driver.Value{viewName, "ZTZS_ERP2"}, rows: [][]driver.Value{{ddl}}, }, }) s := newServer() s.db = db result, err := s.getObjectSource("ZTZS_ERP2", viewName, "VIEW") if err != nil { t.Fatal(err) } if result["source"] != ddl { t.Fatalf("unexpected view source: %#v", result["source"]) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } }) } } func TestGetObjectSourceFallsBackToAllViewsWithOriginalName(t *testing.T) { const viewName = "vEnginWJZ" const source = `SELECT source_id FROM "ZTZS_ERP2"."SOURCE_TABLE"` db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "DBMS_METADATA.GET_DDL('VIEW'", args: []driver.Value{viewName, "ZTZS_ERP2"}, err: errors.New("ORA-31603: object not found"), }, { queryContains: "FROM ALL_VIEWS", args: []driver.Value{"ZTZS_ERP2", viewName}, rows: [][]driver.Value{{source}}, }, }) s := newServer() s.db = db result, err := s.getObjectSource("ZTZS_ERP2", viewName, "VIEW") if err != nil { t.Fatal(err) } if result["source"] != source { t.Fatalf("unexpected fallback source: %#v", result["source"]) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestGetObjectSourceRejectsMissingViewSource(t *testing.T) { db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{ { queryContains: "DBMS_METADATA.GET_DDL('VIEW'", args: []driver.Value{"vEnginWJZ", "ZTZS_ERP2"}, err: errors.New("ORA-31603: object not found"), }, { queryContains: "FROM ALL_VIEWS", args: []driver.Value{"ZTZS_ERP2", "vEnginWJZ"}, rows: nil, }, }) s := newServer() s.db = db result, err := s.getObjectSource("ZTZS_ERP2", "vEnginWJZ", "VIEW") if err == nil || !strings.Contains(err.Error(), "view source not found") { t.Fatalf("expected missing view source error, got result=%#v error=%v", result, err) } if result != nil { t.Fatalf("missing view source must not return a successful empty result: %#v", result) } if scripted.next != len(scripted.steps) { t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next) } } func TestOracleObjectIdentityNameCandidates(t *testing.T) { if got := oracleObjectIdentityNameCandidates("MIXEDPROC"); len(got) != 1 || got[0] != "MIXEDPROC" { t.Fatalf("uppercase identity should be single candidate, got %#v", got) } if got := oracleObjectIdentityNameCandidates("MiXeDProc"); len(got) != 2 || got[0] != "MiXeDProc" || got[1] != "MIXEDPROC" { t.Fatalf("mixed-case identity should try exact then upper, got %#v", got) } if got := oracleObjectIdentityNameCandidates(" "); got != nil { t.Fatalf("blank name should yield no candidates, got %#v", got) } } func contains(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false } type oracleViewSourceQueryStep struct { queryContains string args []driver.Value columns []string rows [][]driver.Value err error } type oracleViewSourceDriver struct { steps []oracleViewSourceQueryStep next int } func (d *oracleViewSourceDriver) Open(string) (driver.Conn, error) { return &oracleViewSourceConn{driver: d}, nil } type oracleViewSourceConn struct { driver *oracleViewSourceDriver } func (c *oracleViewSourceConn) Prepare(string) (driver.Stmt, error) { return nil, errors.New("use QueryContext directly") } func (c *oracleViewSourceConn) Close() error { return nil } func (c *oracleViewSourceConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") } func (c *oracleViewSourceConn) QueryContext( _ context.Context, query string, args []driver.NamedValue, ) (driver.Rows, error) { if c.driver.next >= len(c.driver.steps) { return nil, errors.New("unexpected extra query: " + query) } step := c.driver.steps[c.driver.next] c.driver.next++ if !strings.Contains(query, step.queryContains) { return nil, errors.New("unexpected query: " + query) } values := make([]driver.Value, len(args)) for index, arg := range args { values[index] = arg.Value } if !reflect.DeepEqual(values, step.args) { return nil, errors.New("unexpected query arguments") } if step.err != nil { return nil, step.err } columns := step.columns if len(columns) == 0 { columns = []string{"SOURCE"} } return &oracleViewSourceRows{columns: columns, values: step.rows}, nil } type oracleViewSourceRows struct { columns []string values [][]driver.Value next int } func (r *oracleViewSourceRows) Columns() []string { return r.columns } func (r *oracleViewSourceRows) Close() error { return nil } func (r *oracleViewSourceRows) Next(dest []driver.Value) error { if r.next >= len(r.values) { return io.EOF } copy(dest, r.values[r.next]) r.next++ return nil } func openOracleViewSourceTestDB( t *testing.T, steps []oracleViewSourceQueryStep, ) (*sql.DB, *oracleViewSourceDriver) { t.Helper() driverName := "oracle-test-view-source-" + strings.ReplaceAll(t.Name(), "/", "-") + "-" + time.Now().Format("150405.000000000") scripted := &oracleViewSourceDriver{steps: steps} sql.Register(driverName, scripted) db, err := sql.Open(driverName, "") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) return db, scripted } // -- 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) } }