package main import ( "bufio" "context" "database/sql" "database/sql/driver" "encoding/json" "errors" "fmt" "io" "math" "net" "net/url" "os" "regexp" "sort" "strconv" "strings" "sync" "time" _ "github.com/sijms/go-ora/v2" go_ora "github.com/sijms/go-ora/v2" "github.com/sijms/go-ora/v2/converters" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/transform" ) const protocolVersion = 1 const multiSessionProtocolVersion = 2 const defaultMaxRows = 1000 const oracleDefaultPrefetchRows = "100" const oracleCharsetZHS32GB18030 = 854 const legacyAgentSessionID = "__legacy__" const maxAgentSessions = 256 const oracleLegacyLOBMaxMajorVersion = 10 const oracleDatabaseVersionProbeTimeout = 3 * time.Second const oracleDatabaseVersionSQL = ` SELECT VERSION FROM PRODUCT_COMPONENT_VERSION WHERE PRODUCT LIKE 'Oracle Database%' AND ROWNUM = 1` const oracleDisableSegmentAttributesSQL = `BEGIN DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES', FALSE); END;` const oracleEnableSegmentAttributesSQL = `BEGIN DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES', TRUE); END;` var ( oraclePlSQLBlockStartRegexp = regexp.MustCompile(`(?is)^\s*(?:DECLARE|BEGIN|CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:EDITIONABLE|NONEDITIONABLE)\s+)?(?:FUNCTION|PROCEDURE|TRIGGER|PACKAGE(?:\s+BODY)?|TYPE(?:\s+BODY)?))\b`) oraclePlSQLBlockEndRegexp = regexp.MustCompile(`(?is)\bEND\s*;\s*$`) oracleNamedPlSQLBlockEndRegexp = regexp.MustCompile(`(?is)\bEND\s+([A-Z0-9_$#]+)\s*;\s*$`) oracleUnsupportedServerCharsetRegexp = regexp.MustCompile(`server use charset with id: ([0-9]+).*not supported by the driver`) oracleVersionNumberRegexp = regexp.MustCompile(`(?:^|[^0-9])([0-9]+)\.[0-9]+`) oracleNotNullConstraintRegexp = regexp.MustCompile(`(?i)^\s*\(*\s*(?:"((?:[^"]|"")*)"|([A-Z0-9_$#]+))\s+IS\s+NOT\s+NULL\s*\)*\s*$`) oracleDatabaseVersionQueries = []string{ oracleDatabaseVersionSQL, `SELECT BANNER FROM V$VERSION WHERE BANNER LIKE 'Oracle Database%' AND ROWNUM = 1`, } oracleStringConverters = map[int]converters.IStringConverter{ oracleCharsetZHS32GB18030: oracleGB18030Converter{}, } ) const oracleListDatabasesSQL = ` SELECT username AS owner FROM all_users WHERE username IS NOT NULL AND username NOT IN ( 'SYS','SYSTEM','SYSMAN','DBSNMP','SYSBACKUP','SYSDG','SYSKM','SYSRAC','OUTLN', 'AUDSYS','LBACSYS','DVF','DVSYS','APPQOSSYS','CTXSYS','MDSYS','MDDATA', 'ORDSYS','ORDDATA','ORDPLUGINS','XDB','ANONYMOUS','EXFSYS', 'GSMADMIN_INTERNAL','GSMCATUSER','GSMROOTUSER','GSMUSER','OJVMSYS','OLAPSYS', 'ORACLE_OCM','SI_INFORMTN_SCHEMA','WMSYS','XS$NULL','DBSFWUSER', 'REMOTE_SCHEDULER_AGENT','PDBADMIN','DGPDB_INT','OPS$ORACLE', 'GGSYS','FLOWS_FILES','APEX_PUBLIC_USER' ) AND username NOT LIKE 'APEX_%' AND username NOT LIKE 'FLOWS_%' AND username NOT LIKE '%$%' ORDER BY CASE WHEN username = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') THEN 0 WHEN username = SYS_CONTEXT('USERENV', 'SESSION_USER') THEN 1 ELSE 2 END, username` // Oracle schemas are users, so expose every user visible through ALL_USERS; system filtering remains database-picker behavior. const oracleListSchemasSQL = ` SELECT username AS owner FROM all_users WHERE username IS NOT NULL ORDER BY CASE WHEN username = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') THEN 0 WHEN username = SYS_CONTEXT('USERENV', 'SESSION_USER') THEN 1 ELSE 2 END, username` // Sorting SYS views through ALL_OBJECTS can take over a minute on Oracle 11g; use the specialized dictionaries. const oracleListTablesBaseSQL = ` SELECT OBJECT_NAME, TABLE_TYPE, COMMENTS FROM ( SELECT t.TABLE_NAME AS OBJECT_NAME, 'TABLE' AS TABLE_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM ALL_TABLES t WHERE t.OWNER = :1 AND t.NESTED = 'NO' AND NOT EXISTS ( SELECT 1 FROM ALL_MVIEWS mv WHERE mv.OWNER = t.OWNER AND mv.MVIEW_NAME = t.TABLE_NAME ) UNION ALL SELECT v.VIEW_NAME AS OBJECT_NAME, 'VIEW' AS TABLE_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM ALL_VIEWS v WHERE v.OWNER = :2 UNION ALL SELECT mv.MVIEW_NAME AS OBJECT_NAME, 'MATERIALIZED_VIEW' AS TABLE_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM ALL_MVIEWS mv WHERE mv.OWNER = :3 )` const oracleListTablesSessionUserBaseSQL = ` SELECT OBJECT_NAME, TABLE_TYPE, COMMENTS FROM ( SELECT t.TABLE_NAME AS OBJECT_NAME, 'TABLE' AS TABLE_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_TABLES t WHERE t.NESTED = 'NO' AND NOT EXISTS ( SELECT 1 FROM USER_MVIEWS mv WHERE mv.MVIEW_NAME = t.TABLE_NAME ) UNION ALL SELECT v.VIEW_NAME AS OBJECT_NAME, 'VIEW' AS TABLE_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_VIEWS v UNION ALL SELECT mv.MVIEW_NAME AS OBJECT_NAME, 'MATERIALIZED_VIEW' AS TABLE_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_MVIEWS mv )` const oracleListTablesOrderSQL = `ORDER BY OBJECT_NAME` const oracleListTablesSQL = oracleListTablesBaseSQL + "\n" + oracleListTablesOrderSQL const oracleListObjectsBaseSQL = ` SELECT OBJECT_NAME, OBJECT_TYPE, COMMENTS FROM ( SELECT t.TABLE_NAME AS OBJECT_NAME, 'TABLE' AS OBJECT_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM ALL_TABLES t WHERE t.OWNER = :1 AND t.NESTED = 'NO' AND NOT EXISTS ( SELECT 1 FROM ALL_OBJECTS mv WHERE mv.OWNER = t.OWNER AND mv.OBJECT_NAME = t.TABLE_NAME AND mv.OBJECT_TYPE = 'MATERIALIZED VIEW' ) UNION ALL SELECT o.OBJECT_NAME, CASE o.OBJECT_TYPE WHEN 'PACKAGE BODY' THEN 'PACKAGE_BODY' WHEN 'MATERIALIZED VIEW' THEN 'MATERIALIZED_VIEW' ELSE o.OBJECT_TYPE END AS OBJECT_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM ALL_OBJECTS o WHERE o.OWNER = :2 AND o.OBJECT_TYPE IN ('VIEW', 'MATERIALIZED VIEW', 'PROCEDURE', 'FUNCTION', 'SEQUENCE', 'PACKAGE', 'PACKAGE BODY') UNION ALL SELECT s.SYNONYM_NAME AS OBJECT_NAME, 'SYNONYM' AS OBJECT_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM ALL_SYNONYMS s WHERE s.OWNER = :3 )` const oracleListObjectsSessionUserBaseSQL = ` SELECT OBJECT_NAME, OBJECT_TYPE, COMMENTS FROM ( SELECT t.TABLE_NAME AS OBJECT_NAME, 'TABLE' AS OBJECT_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_TABLES t WHERE t.NESTED = 'NO' AND NOT EXISTS ( SELECT 1 FROM USER_OBJECTS mv WHERE mv.OBJECT_NAME = t.TABLE_NAME AND mv.OBJECT_TYPE = 'MATERIALIZED VIEW' ) UNION ALL SELECT o.OBJECT_NAME, CASE o.OBJECT_TYPE WHEN 'PACKAGE BODY' THEN 'PACKAGE_BODY' WHEN 'MATERIALIZED VIEW' THEN 'MATERIALIZED_VIEW' ELSE o.OBJECT_TYPE END AS OBJECT_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_OBJECTS o WHERE o.OBJECT_TYPE IN ('VIEW', 'MATERIALIZED VIEW', 'PROCEDURE', 'FUNCTION', 'SEQUENCE', 'PACKAGE', 'PACKAGE BODY') UNION ALL SELECT s.SYNONYM_NAME AS OBJECT_NAME, 'SYNONYM' AS OBJECT_TYPE, CAST(NULL AS VARCHAR2(4000)) AS COMMENTS FROM USER_SYNONYMS s )` const oracleListObjectsOrderSQL = `ORDER BY CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 WHEN 'MATERIALIZED_VIEW' THEN 2 WHEN 'PROCEDURE' THEN 3 WHEN 'FUNCTION' THEN 4 WHEN 'SEQUENCE' THEN 5 WHEN 'SYNONYM' THEN 6 WHEN 'PACKAGE' THEN 7 ELSE 8 END, OBJECT_NAME` const oracleListObjectsSQL = oracleListObjectsBaseSQL + "\n" + oracleListObjectsOrderSQL const oracleListTriggersSQL = ` SELECT t.TRIGGER_NAME, t.TRIGGERING_EVENT, t.TRIGGER_TYPE, t.DESCRIPTION, s.LINE, s.TEXT FROM ALL_TRIGGERS t LEFT JOIN ALL_SOURCE s ON s.OWNER = t.OWNER AND s.NAME = t.TRIGGER_NAME AND s.TYPE = 'TRIGGER' WHERE t.OWNER = :1 AND t.TABLE_NAME = :2 AND t.BASE_OBJECT_TYPE IN ('TABLE', 'VIEW') ORDER BY t.TRIGGER_NAME, s.LINE` type request struct { ID json.RawMessage `json:"id"` Method string `json:"method"` Params map[string]json.RawMessage `json:"params"` } type response struct { JSONRPC string `json:"jsonrpc,omitempty"` ID json.RawMessage `json:"id,omitempty"` Result any `json:"result,omitempty"` Error *rpcError `json:"error,omitempty"` } type rpcError struct { Code int `json:"code"` Message string `json:"message"` } type connectParams struct { Host string `json:"host"` Port int `json:"port"` Database string `json:"database"` Username string `json:"username"` Password string `json:"password"` SysDBA bool `json:"sysdba"` URLParams string `json:"url_params"` ConnectionString string `json:"connection_string"` } type completionAssistantRequest struct { ConnectionID string `json:"connection_id"` Database string `json:"database"` Schema string `json:"schema"` ObjectKinds []string `json:"object_kinds"` Mask string `json:"mask"` CaseSensitive bool `json:"case_sensitive"` GlobalSearch bool `json:"global_search"` MaxResults int `json:"max_results"` ParentSchema string `json:"parent_schema"` ParentName string `json:"parent_name"` MatchMode string `json:"match_mode"` } type completionAssistantCandidate struct { Name string `json:"name"` Kind string `json:"kind"` Database *string `json:"database"` Schema *string `json:"schema"` ParentSchema *string `json:"parent_schema"` ParentName *string `json:"parent_name"` Comment *string `json:"comment"` DataType *string `json:"data_type"` Signature *string `json:"signature"` } type completionAssistantResponse struct { Candidates []completionAssistantCandidate `json:"candidates"` Incomplete bool `json:"incomplete"` FallbackUsed bool `json:"fallback_used"` } type queryOptions struct { SQL string `json:"sql"` Database string `json:"database"` Schema string `json:"schema"` MaxRows int `json:"maxRows"` FetchSize int `json:"fetchSize"` TimeoutSecs int `json:"timeoutSecs"` DeferLOBs bool `json:"deferLobs"` } const largeValueBytesColumnPrefix = "__DBX_LARGE_VALUE_BYTES_" type queryResult struct { Columns []string `json:"columns"` ColumnTypes []string `json:"column_types"` Rows [][]any `json:"rows"` AffectedRows int64 `json:"affected_rows"` ExecutionTimeMS int64 `json:"execution_time_ms"` Truncated bool `json:"truncated"` } func (r queryResult) MarshalJSON() ([]byte, error) { type alias queryResult value := alias(r) if value.Columns == nil { value.Columns = []string{} } if value.ColumnTypes == nil { value.ColumnTypes = []string{} } if value.Rows == nil { value.Rows = [][]any{} } data, err := json.Marshal(value) if err == nil { return data, nil } rows, changed := normalizeNonFiniteQueryRows(value.Rows) if !changed { return nil, err } value.Rows = rows return json.Marshal(value) } type queryPageResult struct { Columns []string `json:"columns"` ColumnTypes []string `json:"column_types"` Rows [][]any `json:"rows"` AffectedRows int64 `json:"affected_rows"` ExecutionTimeMS int64 `json:"execution_time_ms"` Truncated bool `json:"truncated"` SessionID *string `json:"session_id"` HasMore bool `json:"has_more"` } func (r queryPageResult) MarshalJSON() ([]byte, error) { type alias queryPageResult value := alias(r) if value.Columns == nil { value.Columns = []string{} } if value.ColumnTypes == nil { value.ColumnTypes = []string{} } if value.Rows == nil { value.Rows = [][]any{} } data, err := json.Marshal(value) if err == nil { return data, nil } rows, changed := normalizeNonFiniteQueryRows(value.Rows) if !changed { return nil, err } value.Rows = rows return json.Marshal(value) } func normalizeNonFiniteQueryRows(rows [][]any) ([][]any, bool) { result := rows changed := false for rowIndex, row := range rows { var normalizedRow []any for columnIndex, value := range row { floatValue, ok := value.(float64) if !ok || (!math.IsNaN(floatValue) && !math.IsInf(floatValue, 0)) { continue } if normalizedRow == nil { normalizedRow = append([]any(nil), row...) } normalizedRow[columnIndex] = fmt.Sprint(floatValue) } if normalizedRow != nil { if !changed { result = append([][]any(nil), rows...) changed = true } result[rowIndex] = normalizedRow } } return result, changed } type querySession struct { rows *sql.Rows columns []string columnTypes []string pending []any remaining int } type oracleColumnMeta struct { Name string DataType string } type oracleColumnMetaLoader func(schema, table string) ([]oracleColumnMeta, error) type databaseInfo struct { Name string `json:"name"` } type tableInfo struct { Name string `json:"name"` TableType string `json:"table_type"` Comment *string `json:"comment"` } type metadataListConstraints struct { Filter string Limit int Offset int ObjectTypes []string } type objectInfo struct { Name string `json:"name"` ObjectType string `json:"object_type"` Schema string `json:"schema"` Comment *string `json:"comment"` } type columnInfo struct { Name string `json:"name"` DataType string `json:"data_type"` IsNullable bool `json:"is_nullable"` ColumnDefault *string `json:"column_default"` IsPrimaryKey bool `json:"is_primary_key"` Extra *string `json:"extra"` Comment *string `json:"comment"` NumericPrecision *int `json:"numeric_precision"` NumericScale *int `json:"numeric_scale"` CharacterMaximumLength *int `json:"character_maximum_length"` CharacterLengthUnit *string `json:"-"` } type indexInfo struct { Name string `json:"name"` Columns []string `json:"columns"` IsUnique bool `json:"is_unique"` IsPrimary bool `json:"is_primary"` Filter *string `json:"filter"` IndexType *string `json:"index_type"` IncludedColumns []string `json:"included_columns"` Comment *string `json:"comment"` } func (i indexInfo) MarshalJSON() ([]byte, error) { type alias indexInfo value := alias(i) if value.Columns == nil { value.Columns = []string{} } if value.IncludedColumns == nil { value.IncludedColumns = []string{} } return json.Marshal(value) } type foreignKeyInfo struct { Name string `json:"name"` Column string `json:"column"` RefSchema string `json:"ref_schema"` RefTable string `json:"ref_table"` RefColumn string `json:"ref_column"` OnDelete string `json:"on_delete"` } type triggerInfo struct { Name string `json:"name"` Event string `json:"event"` Timing string `json:"timing"` Statement *string `json:"statement,omitempty"` } // constraintInfo represents primary key, unique, and check constraints for a // table. Foreign keys are served separately by listForeignKeys, so this only // covers constraint types 'P', 'U', and 'C'. type constraintInfo struct { Name string `json:"name"` ConstraintType string `json:"constraint_type"` Definition string `json:"definition"` Columns []string `json:"columns"` RefSchema *string `json:"ref_schema,omitempty"` RefTable *string `json:"ref_table,omitempty"` RefColumns []string `json:"ref_columns"` MatchType *string `json:"match_type,omitempty"` OnUpdate *string `json:"on_update,omitempty"` OnDelete *string `json:"on_delete,omitempty"` Deferrable bool `json:"deferrable"` InitiallyDeferred bool `json:"initially_deferred"` Enabled bool `json:"enabled"` Valid bool `json:"valid"` } func (c constraintInfo) MarshalJSON() ([]byte, error) { type alias constraintInfo value := alias(c) if value.Columns == nil { value.Columns = []string{} } if value.RefColumns == nil { value.RefColumns = []string{} } return json.Marshal(value) } type server struct { db *sql.DB params connectParams legacyLOBFetchDeferred bool // manualConn + manualTx pin one physical Oracle session for interactive // commit/rollback control across multiple execute_query RPCs. manualConn *sql.Conn manualTx *sql.Tx sessions map[string]*querySession tableReadSessions map[string]*querySession nextSessionID int64 nextTableReadSessionID int64 activeCancelMu sync.Mutex activeCancel context.CancelFunc activeRows map[*sql.Rows]context.CancelFunc activeTimer *time.Timer activeTimedOut bool } type agentSession struct { server *server mu sync.Mutex } type runtimeServer struct { mu sync.RWMutex sessions map[string]*agentSession } func main() { runtime := newRuntimeServer() encoder := json.NewEncoder(os.Stdout) var encoderMu sync.Mutex var requests sync.WaitGroup shutdown := make(chan struct{}) var shutdownOnce sync.Once fmt.Fprintln(os.Stdout, `{"ready":true}`) scanner := bufio.NewScanner(os.Stdin) scanner.Buffer(make([]byte, 0, 64*1024), 512*1024*1024) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } var requestEnvelope request if json.Unmarshal([]byte(line), &requestEnvelope) == nil && requestEnvelope.Method == "shutdown" { requests.Wait() resp, _ := runtime.handleLine(line) encoderMu.Lock() err := encoder.Encode(resp) encoderMu.Unlock() if err != nil { fmt.Fprintf(os.Stderr, "failed to write response: %v\n", err) } return } requests.Add(1) go func() { defer requests.Done() resp, shouldShutdown := runtime.handleLine(line) encoderMu.Lock() err := encoder.Encode(resp) encoderMu.Unlock() if err != nil { fmt.Fprintf(os.Stderr, "failed to write response: %v\n", err) shutdownOnce.Do(func() { close(shutdown) }) return } if shouldShutdown { shutdownOnce.Do(func() { close(shutdown) }) } }() select { case <-shutdown: requests.Wait() return default: } } requests.Wait() if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) { fmt.Fprintf(os.Stderr, "failed to read stdin: %v\n", err) } } func newRuntimeServer() *runtimeServer { return &runtimeServer{sessions: map[string]*agentSession{}} } func (r *runtimeServer) handleLine(line string) (response, bool) { var req request if err := json.Unmarshal([]byte(line), &req); err != nil { return errorResponse(nil, err), false } if len(req.ID) == 0 { req.ID = json.RawMessage("1") } result, shutdown, err := r.dispatch(req.Method, req.Params) if err != nil { return errorResponse(req.ID, err), false } return response{JSONRPC: "2.0", ID: req.ID, Result: result}, shutdown } func (r *runtimeServer) dispatch(method string, params map[string]json.RawMessage) (any, bool, error) { switch method { case "handshake": return map[string]any{ "protocolVersion": multiSessionProtocolVersion, "agentProtocolVersion": multiSessionProtocolVersion, "capabilities": []string{"connect", "test_connection", "metadata", "query", "transaction", "ddl", "multi_session"}, }, false, nil case "open_session": agentSessionID := stringParam(params, "agentSessionId") if agentSessionID == "" { return nil, false, errors.New("agentSessionId is required") } var connectParams connectParams if err := decodeParams(params, &connectParams); err != nil { return nil, false, err } return map[string]bool{"ok": true}, false, r.openSession(agentSessionID, connectParams) case "close_session": return map[string]bool{"ok": true}, false, r.closeSession(stringParam(params, "agentSessionId")) case "validate_session": agentSessionID := stringParam(params, "agentSessionId") session, err := r.session(agentSessionID) if err != nil { return nil, false, err } session.mu.Lock() defer session.mu.Unlock() if _, _, err := session.server.dispatch("validate_connection", params); err == nil { return map[string]bool{"ok": true}, false, nil } // Reconnect only this logical session. Other sessions in the runtime keep // their connections, transactions, cursors, and in-flight requests. if err := session.server.connect(session.server.params); err != nil { return nil, false, err } return map[string]bool{"ok": true}, false, nil case "cancel_session": session, err := r.session(stringParam(params, "agentSessionId")) if err != nil { return nil, false, err } session.server.cancelActiveQuery() return map[string]bool{"ok": true}, false, nil case "test_connection": return newServer().dispatch(method, params) case "shutdown": return map[string]bool{"ok": true}, true, r.closeAllSessions() case "connect": var connectParams connectParams if err := decodeParams(params, &connectParams); err != nil { return nil, false, err } return map[string]bool{"ok": true}, false, r.replaceSession(legacyAgentSessionID, connectParams) case "disconnect": return map[string]bool{"ok": true}, false, r.closeSession(legacyAgentSessionID) default: agentSessionID := stringParam(params, "agentSessionId") if agentSessionID == "" { agentSessionID = legacyAgentSessionID } return r.withSession(agentSessionID, method, params) } } func (r *runtimeServer) withSession(agentSessionID, method string, params map[string]json.RawMessage) (any, bool, error) { session, err := r.session(agentSessionID) if err != nil { return nil, false, err } // Oracle connection state, transactions, and cursors are session-scoped; // serialize one session while allowing separate sessions to run in parallel. session.mu.Lock() defer session.mu.Unlock() return session.server.dispatch(method, params) } func (r *runtimeServer) openSession(agentSessionID string, params connectParams) error { r.mu.Lock() if _, exists := r.sessions[agentSessionID]; exists { r.mu.Unlock() return fmt.Errorf("agent session already exists: %s", agentSessionID) } if len(r.sessions) >= maxAgentSessions { r.mu.Unlock() return fmt.Errorf("agent session limit reached: %d", maxAgentSessions) } session := &agentSession{server: newServer()} r.sessions[agentSessionID] = session r.mu.Unlock() // Reserve the id under the registry lock, then connect outside it so unrelated // sessions can establish database connections concurrently. session.mu.Lock() err := session.server.connect(params) session.mu.Unlock() if err != nil { r.mu.Lock() if r.sessions[agentSessionID] == session { delete(r.sessions, agentSessionID) } r.mu.Unlock() return err } return nil } func (r *runtimeServer) replaceSession(agentSessionID string, params connectParams) error { _ = r.closeSession(agentSessionID) return r.openSession(agentSessionID, params) } func (r *runtimeServer) session(agentSessionID string) (*agentSession, error) { if agentSessionID == "" { return nil, errors.New("agentSessionId is required") } r.mu.RLock() session := r.sessions[agentSessionID] r.mu.RUnlock() if session == nil { return nil, fmt.Errorf("agent session not found: %s", agentSessionID) } return session, nil } func (r *runtimeServer) closeSession(agentSessionID string) error { if agentSessionID == "" { return errors.New("agentSessionId is required") } r.mu.Lock() session := r.sessions[agentSessionID] delete(r.sessions, agentSessionID) r.mu.Unlock() if session == nil { return nil } session.mu.Lock() defer session.mu.Unlock() return session.server.disconnect() } func (r *runtimeServer) closeAllSessions() error { r.mu.Lock() sessions := r.sessions r.sessions = map[string]*agentSession{} r.mu.Unlock() var firstErr error for _, session := range sessions { session.mu.Lock() err := session.server.disconnect() session.mu.Unlock() if firstErr == nil && err != nil { firstErr = err } } return firstErr } func newServer() *server { return &server{ sessions: map[string]*querySession{}, tableReadSessions: map[string]*querySession{}, activeRows: map[*sql.Rows]context.CancelFunc{}, } } func (s *server) handleLine(line string) (response, bool) { var req request if err := json.Unmarshal([]byte(line), &req); err != nil { return errorResponse(nil, err), false } if len(req.ID) == 0 { req.ID = json.RawMessage("1") } result, shutdown, err := s.dispatch(req.Method, req.Params) if err != nil { return errorResponse(req.ID, err), false } return response{JSONRPC: "2.0", ID: req.ID, Result: result}, shutdown } func (s *server) dispatch(method string, params map[string]json.RawMessage) (any, bool, error) { if oracleMethodMayReadLOB(method) { if err := s.ensureLegacyOracleLOBFetch(); err != nil { return nil, false, err } } switch method { case "handshake": return map[string]any{ "protocolVersion": protocolVersion, "agentProtocolVersion": protocolVersion, "capabilities": []string{"connect", "test_connection", "metadata", "query", "transaction", "ddl"}, }, false, nil case "connect": var cp connectParams if err := decodeParams(params, &cp); err != nil { return nil, false, err } return map[string]bool{"ok": true}, false, s.connect(cp) case "test_connection": var cp connectParams if err := decodeParams(params, &cp); err != nil { return nil, false, err } db, err := openAndPingDB(cp, 5*time.Second) if err != nil { return nil, false, err } defer db.Close() return map[string]bool{"ok": true}, false, nil case "validate_connection": if s.db == nil { return nil, false, errors.New("not connected") } return map[string]bool{"ok": true}, false, pingDB(s.db, 5*time.Second) case "list_databases": result, err := s.listDatabases() return result, false, err case "list_schemas": result, err := s.listSchemas(stringSliceParam(params, "visible_schemas")) return result, false, err case "list_tables": schema := stringParam(params, "schema") result, err := s.listTables(schema, metadataListConstraintsFromParams(params)) return result, false, err case "list_objects": schema := stringParam(params, "schema") result, err := s.listObjects(schema, metadataListConstraintsFromParams(params)) return result, false, err case "completion_assistant_search_v1": var request completionAssistantRequest if err := decodeParams(params, &request); err != nil { return nil, false, err } result, err := s.completionAssistantSearch(request) return result, false, err case "get_columns": schema := stringParam(params, "schema") table := stringParam(params, "table") result, err := s.getColumns(schema, table) return result, false, err case "get_table_comment": schema := stringParam(params, "schema") table := stringParam(params, "table") result, err := s.getTableComment(schema, table) return result, false, err case "get_object_source": schema := stringParam(params, "schema") name := stringParam(params, "name") objectType := stringParam(params, "object_type") source, err := s.getObjectSource(schema, name, objectType) return source, false, err case "get_table_ddl": schema := stringParam(params, "schema") table := stringParam(params, "table") objectType := stringParam(params, "object_type") ddl, err := s.getTableDDLWithOptions(schema, table, objectType, boolParam(params, "portable")) return ddl, false, err case "execute_query": var opts queryOptions if err := decodeParams(params, &opts); err != nil { return nil, false, err } result, err := s.executeQuery(opts) return result, false, err case "execute_query_page": var opts queryOptions if err := decodeParams(params, &opts); err != nil { return nil, false, err } result, err := s.executeQueryPage(opts, intParam(params, "pageSize")) return result, false, err case "fetch_query_page": result, err := s.fetchQueryPage(stringParam(params, "sessionId"), intParam(params, "pageSize")) return result, false, err case "close_query_session": return s.closeQuerySession(stringParam(params, "sessionId")), false, nil case "start_table_read": var opts queryOptions if err := decodeParams(params, &opts); err != nil { return nil, false, err } result, err := s.startTableRead(opts, intParam(params, "pageSize")) return result, false, err case "fetch_table_read_page": result, err := s.fetchTableReadPage(stringParam(params, "sessionId"), intParam(params, "pageSize")) return result, false, err case "close_table_read_session": return s.closeTableReadSession(stringParam(params, "sessionId")), false, nil case "list_indexes": schema := stringParam(params, "schema") table := stringParam(params, "table") result, err := s.listIndexes(schema, table) return result, false, err case "list_foreign_keys": schema := stringParam(params, "schema") table := stringParam(params, "table") result, err := s.listForeignKeys(schema, table) return result, false, err case "list_constraints": schema := stringParam(params, "schema") table := stringParam(params, "table") result, err := s.listConstraints(schema, table) return result, false, err case "list_triggers": schema := stringParam(params, "schema") table := stringParam(params, "table") result, err := s.listTriggers(schema, table) return result, false, err case "get_explain_info": sqlText := stringParam(params, "sql") plan, err := s.getExplainInfo( sqlText, stringParam(params, "database"), stringParam(params, "schema"), intParam(params, "timeoutSecs"), ) return map[string]any{"plan": plan, "has_actual_stats": false}, false, err case "execute_transaction": result, err := s.executeTransaction(params) return result, false, err case "begin_manual_transaction": return map[string]bool{"ok": true}, false, s.beginManualTransaction(stringParam(params, "schema")) case "commit_manual_transaction": return map[string]bool{"ok": true}, false, s.commitManualTransaction() case "rollback_manual_transaction": return map[string]bool{"ok": true}, false, s.rollbackManualTransaction() case "disconnect": return map[string]bool{"ok": true}, false, s.disconnect() case "shutdown": _ = s.disconnect() return map[string]bool{"ok": true}, true, nil default: return nil, false, fmt.Errorf("unknown method: %s", method) } } func (s *server) connect(params connectParams) error { _ = s.disconnect() db, err := openConfiguredSessionDB(params, 15*time.Second) if err != nil { return err } majorVersion, versionKnown := oracleServerMajorVersion(db, 15*time.Second) s.db = db s.params = params s.legacyLOBFetchDeferred = shouldUseLegacyOracleLOBFetch(params, majorVersion, versionKnown) return nil } func openConfiguredSessionDB(params connectParams, timeout time.Duration) (*sql.DB, error) { db, err := openAndPingDB(params, timeout) if err != nil { return nil, err } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if _, err := db.ExecContext(ctx, "ALTER SESSION SET NLS_LANGUAGE='AMERICAN'"); err != nil { db.Close() return nil, err } return db, nil } func oracleServerMajorVersion(db *sql.DB, timeout time.Duration) (int, bool) { if timeout > oracleDatabaseVersionProbeTimeout { timeout = oracleDatabaseVersionProbeTimeout } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if majorVersion, ok := oracleServerMajorVersionFromDBConn(ctx, db); ok { return majorVersion, true } for _, query := range oracleDatabaseVersionQueries { var version string err := db.QueryRowContext(ctx, query).Scan(&version) if err == nil { if majorVersion, ok := parseOracleMajorVersion(version); ok { return majorVersion, true } } } return 0, false } func oracleServerMajorVersionFromDBConn(ctx context.Context, db *sql.DB) (int, bool) { conn, err := db.Conn(ctx) if err != nil { return 0, false } defer conn.Close() var majorVersion int var versionKnown bool if err := conn.Raw(func(driverConn any) error { majorVersion, versionKnown = oracleServerMajorVersionFromDriverConn(driverConn) return nil }); err != nil { return 0, false } return majorVersion, versionKnown } func oracleServerMajorVersionFromDriverConn(driverConn any) (int, bool) { conn, ok := driverConn.(*go_ora.Connection) if !ok { return 0, false } return parseOracleAuthVersionNumber(conn.SessionProperties["AUTH_VERSION_NO"]) } func parseOracleAuthVersionNumber(value string) (int, bool) { encoded, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32) if err != nil { return 0, false } majorVersion := int(encoded >> 24) return majorVersion, majorVersion > 0 } func parseOracleMajorVersion(version string) (int, bool) { match := oracleVersionNumberRegexp.FindStringSubmatch(strings.TrimSpace(version)) if len(match) != 2 { return 0, false } value, err := strconv.Atoi(match[1]) return value, err == nil && value > 0 } func shouldUseLegacyOracleLOBFetch(params connectParams, majorVersion int, versionKnown bool) bool { return versionKnown && majorVersion <= oracleLegacyLOBMaxMajorVersion && !hasOracleLOBFetchOption(params) } func oracleMethodMayReadLOB(method string) bool { switch method { case "get_table_ddl", "execute_query", "execute_query_page", "start_table_read", "execute_transaction": return true default: return false } } func (s *server) ensureLegacyOracleLOBFetch() error { if !s.legacyLOBFetchDeferred { return nil } effectiveParams := withOracleLOBFetchPost(s.params) if effectiveParams == s.params { s.legacyLOBFetchDeferred = false return nil } db, err := openConfiguredSessionDB(effectiveParams, 15*time.Second) if err != nil { return err } oldDB := s.db s.db = db s.params = effectiveParams s.legacyLOBFetchDeferred = false if oldDB != nil { _ = oldDB.Close() } return nil } func hasOracleLOBFetchOption(params connectParams) bool { connectionString := strings.TrimSpace(params.ConnectionString) if strings.HasPrefix(strings.ToLower(connectionString), "oracle://") { parsed, err := url.Parse(connectionString) return err == nil && hasURLValueKey(parsed.Query(), "LOB FETCH") } values, err := url.ParseQuery(params.URLParams) return err == nil && hasURLValueKey(values, "LOB FETCH") } func hasURLValueKey(values url.Values, target string) bool { for key := range values { if strings.EqualFold(strings.TrimSpace(key), target) { return true } } return false } func withOracleLOBFetchPost(params connectParams) connectParams { connectionString := strings.TrimSpace(params.ConnectionString) if strings.HasPrefix(strings.ToLower(connectionString), "oracle://") { parsed, err := url.Parse(connectionString) if err != nil { return params } values := parsed.Query() values.Set("LOB FETCH", "POST") parsed.RawQuery = values.Encode() params.ConnectionString = parsed.String() return params } values, err := url.ParseQuery(params.URLParams) if err != nil { return params } values.Set("LOB FETCH", "POST") params.URLParams = values.Encode() return params } func (s *server) disconnect() error { s.closeAllQuerySessions() _ = s.rollbackManualTransactionQuiet() s.legacyLOBFetchDeferred = false if s.db == nil { return nil } err := s.db.Close() s.db = nil return err } func (s *server) beginManualTransaction(schema string) error { if s.manualTx != nil { return errors.New("manual transaction already open") } db, err := s.requireDB() if err != nil { return err } // Hold one exclusive physical connection so DML/SELECT/schema stay on the // same Oracle session for the life of the interactive transaction. conn, err := db.Conn(context.Background()) if err != nil { return fmt.Errorf("reserve connection for manual transaction: %w", err) } if strings.TrimSpace(schema) != "" { if _, err := conn.ExecContext(context.Background(), "ALTER SESSION SET CURRENT_SCHEMA = "+quoteIdentifier(schema)); err != nil { _ = conn.Close() return err } } tx, err := conn.BeginTx(context.Background(), nil) if err != nil { _ = conn.Close() return fmt.Errorf("begin manual transaction: %w", err) } s.manualConn = conn s.manualTx = tx return nil } func (s *server) commitManualTransaction() error { if s.manualTx == nil { return errors.New("no manual transaction open") } err := s.manualTx.Commit() s.clearManualTransaction() return err } func (s *server) rollbackManualTransaction() error { if s.manualTx == nil { return errors.New("no manual transaction open") } err := s.manualTx.Rollback() s.clearManualTransaction() return err } func (s *server) rollbackManualTransactionQuiet() error { if s.manualTx == nil { return nil } err := s.manualTx.Rollback() s.clearManualTransaction() return err } func (s *server) clearManualTransaction() { s.manualTx = nil if s.manualConn != nil { _ = s.manualConn.Close() s.manualConn = nil } } func (s *server) hasManualTransaction() bool { return s.manualTx != nil } func openDB(params connectParams) (*sql.DB, error) { return openDBWithStringConverter(params, nil) } func openDBWithStringConverter(params connectParams, stringConverter converters.IStringConverter) (*sql.DB, error) { dsn, err := buildDSNForConnect(params) if err != nil { return nil, err } connector := go_ora.NewConnector(dsn) if stringConverter != nil { go_ora.SetStringConverter(connector, stringConverter, nil) } db := sql.OpenDB(connector) db.SetMaxOpenConns(4) db.SetMaxIdleConns(1) db.SetConnMaxLifetime(30 * time.Minute) return db, nil } func openAndPingDB(params connectParams, timeout time.Duration) (*sql.DB, error) { db, err := openDB(params) if err != nil { return nil, err } if err := pingDB(db, timeout); err != nil { db.Close() stringConverter, ok := oracleStringConverterForUnsupportedCharsetError(err) if !ok { return nil, err } // Retry only charsets with explicit converters. Guessing a converter // can make the connection succeed while silently corrupting text. db, err = openDBWithStringConverter(params, stringConverter) if err != nil { return nil, err } if retryErr := pingDB(db, timeout); retryErr != nil { db.Close() return nil, retryErr } } return db, nil } func pingDB(db *sql.DB, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return db.PingContext(ctx) } func oracleStringConverterForUnsupportedCharsetError(err error) (converters.IStringConverter, bool) { charsetID, ok := unsupportedOracleServerCharsetID(err) if !ok { return nil, false } stringConverter, ok := oracleStringConverters[charsetID] return stringConverter, ok } func unsupportedOracleServerCharsetID(err error) (int, bool) { if err == nil { return 0, false } match := oracleUnsupportedServerCharsetRegexp.FindStringSubmatch(err.Error()) if len(match) != 2 { return 0, false } charsetID, parseErr := strconv.Atoi(match[1]) if parseErr != nil { return 0, false } return charsetID, true } type oracleGB18030Converter struct{} func (oracleGB18030Converter) Encode(input string) []byte { output, _, err := transform.String(simplifiedchinese.GB18030.NewEncoder(), input) if err != nil { return []byte(input) } return []byte(output) } func (oracleGB18030Converter) Decode(input []byte) string { output, _, err := transform.Bytes(simplifiedchinese.GB18030.NewDecoder(), input) if err != nil { return string(input) } return string(output) } func (oracleGB18030Converter) GetLangID() int { return oracleCharsetZHS32GB18030 } func (oracleGB18030Converter) Clone() converters.IStringConverter { return oracleGB18030Converter{} } func buildDSN(params connectParams) string { connectionString := strings.TrimSpace(params.ConnectionString) if strings.HasPrefix(strings.ToLower(connectionString), "oracle://") { parsed, err := url.Parse(connectionString) if err != nil { return connectionString } values := parsed.Query() setOracleDefaultPrefetchRows(values) parsed.RawQuery = values.Encode() return parsed.String() } username := params.Username options := parseURLParams(params.URLParams) setOracleDefaultPrefetchRowsMap(options) if params.SysDBA { options["AUTH TYPE"] = "SYSDBA" } if jdbc := parseOracleJDBCURL(connectionString); jdbc.Kind != "" { if jdbc.Descriptor != "" { return buildGoOraJDBC(username, params.Password, jdbc.Descriptor, options) } host := jdbc.Host port := jdbc.Port if port == 0 { port = 1521 } if jdbc.Kind == "sid" { options["SID"] = jdbc.Database return buildGoOraURL(host, port, "", username, params.Password, options) } return buildGoOraURL(host, port, jdbc.Database, username, params.Password, options) } service := oracleConnectionDatabaseName(params.Database) port := params.Port if port == 0 { port = 1521 } return buildGoOraURL(params.Host, port, service, username, params.Password, options) } func setOracleDefaultPrefetchRows(values url.Values) { if hasURLValueKey(values, "PREFETCH_ROWS") { return } values.Set("PREFETCH_ROWS", oracleDefaultPrefetchRows) } func setOracleDefaultPrefetchRowsMap(options map[string]string) { for key := range options { if strings.EqualFold(strings.TrimSpace(key), "PREFETCH_ROWS") { return } } options["PREFETCH_ROWS"] = oracleDefaultPrefetchRows } func oracleConnectionDatabaseName(database string) string { database = strings.TrimSpace(database) if strings.HasPrefix(strings.ToUpper(database), "SYSDBA:") { return strings.TrimSpace(database[len("SYSDBA:"):]) } return database } func buildGoOraJDBC(user, password, connStr string, options map[string]string) string { if options == nil { options = make(map[string]string) } options["connStr"] = connStr return buildGoOraURL("", 0, "", user, password, options) } func buildGoOraURL(server string, port int, service, user, password string, options map[string]string) string { // go-ora v2.9.0 uses path escaping for user/password, leaving ':' unescaped. // Userinfo escaping keeps bastion usernames such as 9008888:reader intact // without changing their authentication semantics. ret := fmt.Sprintf( "oracle://%s@%s/%s", url.UserPassword(user, password).String(), net.JoinHostPort(server, strconv.Itoa(port)), url.PathEscape(service), ) if options != nil { ret += "?" for key, val := range options { val = strings.TrimSpace(val) for _, temp := range strings.Split(val, ",") { temp = strings.TrimSpace(temp) if strings.ToUpper(key) == "SERVER" { ret += fmt.Sprintf("%s=%s&", key, temp) } else { ret += fmt.Sprintf("%s=%s&", key, url.QueryEscape(temp)) } } } ret = strings.TrimRight(ret, "&") } return ret } type jdbcURLInfo struct { Kind string Host string Port int Database string Descriptor string } var ( oracleJDBCServiceRegexp = regexp.MustCompile(`(?i)^jdbc:oracle:thin:@//([^/:]+):([0-9]+)/([^?]+)`) oracleJDBCSIDRegexp = regexp.MustCompile(`(?i)^jdbc:oracle:thin:@([^/:]+):([0-9]+):([^?]+)`) oracleJDBCLegacyRegexp = regexp.MustCompile(`(?i)^jdbc:oracle:thin:@([^/:]+):([0-9]+)/([^?]+)`) ) func parseOracleJDBCURL(value string) jdbcURLInfo { value = strings.TrimSpace(value) lower := strings.ToLower(value) if !strings.HasPrefix(lower, "jdbc:oracle:thin:@") { return jdbcURLInfo{} } descriptor := strings.TrimSpace(value[len("jdbc:oracle:thin:@"):]) if strings.HasPrefix(descriptor, "(") { return jdbcURLInfo{Kind: "descriptor", Descriptor: descriptor} } if match := oracleJDBCServiceRegexp.FindStringSubmatch(value); len(match) == 4 { return jdbcURLInfo{Kind: "service", Host: match[1], Port: parsePort(match[2]), Database: match[3]} } if match := oracleJDBCSIDRegexp.FindStringSubmatch(value); len(match) == 4 { return jdbcURLInfo{Kind: "sid", Host: match[1], Port: parsePort(match[2]), Database: match[3]} } if match := oracleJDBCLegacyRegexp.FindStringSubmatch(value); len(match) == 4 { return jdbcURLInfo{Kind: "service", Host: match[1], Port: parsePort(match[2]), Database: match[3]} } return jdbcURLInfo{} } func parsePort(value string) int { port, _ := strconv.Atoi(value) return port } func (s *server) requireDB() (*sql.DB, error) { if s.db == nil { return nil, errors.New("agent is not connected") } return s.db, nil } func (s *server) listDatabases() ([]databaseInfo, error) { rows, err := s.queryRows(oracleListDatabasesSQL, nil) if err != nil { if isOraclePGALimitError(err) { return s.currentSchemaDatabase() } return nil, err } defer s.closeRows(rows) var result []databaseInfo for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } result = append(result, databaseInfo{Name: name}) } if err := rows.Err(); err != nil { if isOraclePGALimitError(err) { return s.currentSchemaDatabase() } return nil, err } return emptyIfNil(result), nil } func isOraclePGALimitError(err error) bool { return err != nil && strings.Contains(strings.ToUpper(err.Error()), "ORA-04036") } func (s *server) currentSchemaDatabase() ([]databaseInfo, error) { schema, err := s.currentSchema() if err != nil { return nil, err } if strings.TrimSpace(schema) == "" { return []databaseInfo{}, nil } return []databaseInfo{{Name: schema}}, nil } func (s *server) listSchemas(visibleSchemas []string) ([]string, error) { if visibleSchemas != nil && len(visibleSchemas) == 0 { return []string{}, nil } sqlText, args := oracleListSchemasSQLWithVisibleSchemas(visibleSchemas) rows, err := s.queryRows(sqlText, args) if err != nil { if isOraclePGALimitError(err) { databases, fallbackErr := s.currentSchemaDatabase() if fallbackErr != nil { return nil, fallbackErr } result := make([]string, 0, len(databases)) for _, database := range databases { result = append(result, database.Name) } return emptyIfNil(result), nil } return nil, err } defer s.closeRows(rows) var result []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } result = append(result, name) } if err := rows.Err(); err != nil { if isOraclePGALimitError(err) { databases, fallbackErr := s.currentSchemaDatabase() if fallbackErr != nil { return nil, fallbackErr } result = result[:0] for _, database := range databases { result = append(result, database.Name) } return emptyIfNil(result), nil } return nil, err } return emptyIfNil(result), nil } func (s *server) listDatabasesFiltered(visibleSchemas []string) ([]databaseInfo, error) { if visibleSchemas == nil { return s.listDatabases() } sqlText, args := oracleListDatabasesSQLWithVisibleSchemas(visibleSchemas) rows, err := s.queryRows(sqlText, args) if err != nil { if isOraclePGALimitError(err) { return s.currentSchemaDatabase() } return nil, err } defer s.closeRows(rows) var result []databaseInfo for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } result = append(result, databaseInfo{Name: name}) } if err := rows.Err(); err != nil { if isOraclePGALimitError(err) { return s.currentSchemaDatabase() } return nil, err } return emptyIfNil(result), nil } func oracleListDatabasesSQLWithVisibleSchemas(visibleSchemas []string) (string, []any) { return oracleListSQLWithVisibleSchemas(oracleListDatabasesSQL, visibleSchemas) } func oracleListSchemasSQLWithVisibleSchemas(visibleSchemas []string) (string, []any) { return oracleListSQLWithVisibleSchemas(oracleListSchemasSQL, visibleSchemas) } func oracleListSQLWithVisibleSchemas(baseSQL string, visibleSchemas []string) (string, []any) { if len(visibleSchemas) == 0 { return baseSQL, nil } placeholders := make([]string, 0, len(visibleSchemas)) args := make([]any, 0, len(visibleSchemas)) for i, schema := range visibleSchemas { placeholders = append(placeholders, fmt.Sprintf(":%d", i+1)) args = append(args, schema) } sqlText := strings.Replace( baseSQL, "\nORDER BY CASE", "\n AND username IN ("+strings.Join(placeholders, ",")+")\nORDER BY CASE", 1, ) return sqlText, args } func (s *server) currentSchema() (string, error) { db, err := s.requireDB() if err != nil { return "", err } var schema string if err := db.QueryRow("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL").Scan(&schema); err != nil { return "", err } return strings.ToUpper(schema), nil } func (s *server) normalizeSchema(schema string) (string, error) { return resolveOracleSchema(schema, s.currentSchema, s.sessionUser) } func resolveOracleSchema(schema string, currentSchema, sessionUser func() (string, error)) (string, error) { if schema = strings.TrimSpace(schema); schema != "" { return strings.ToUpper(schema), nil } if current, err := currentSchema(); err == nil && strings.TrimSpace(current) != "" { return strings.ToUpper(strings.TrimSpace(current)), nil } return sessionUser() } func (s *server) sessionUser() (string, error) { db, err := s.requireDB() if err != nil { return "", err } var username string if err := db.QueryRow("SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM DUAL").Scan(&username); err != nil { return "", err } return strings.ToUpper(username), nil } func (s *server) schemaIsSessionUser(schema string) bool { username, err := s.sessionUser() return err == nil && strings.EqualFold(schema, username) } type oracleMetadataListQuery struct { SQL string Args []any } func metadataListConstraintsFromParams(params map[string]json.RawMessage) metadataListConstraints { objectTypes := stringSliceParam(params, "object_types") if len(objectTypes) == 0 { objectTypes = stringSliceParam(params, "objectTypes") } limit := intParam(params, "limit") offset := intParam(params, "offset") if limit < 0 { limit = 0 } if offset < 0 { offset = 0 } return metadataListConstraints{ Filter: stringParam(params, "filter"), Limit: limit, Offset: offset, ObjectTypes: objectTypes, } } func oracleListTablesQuery(schema string, constraints metadataListConstraints) oracleMetadataListQuery { return oracleConstrainedMetadataListQuery( oracleListTablesBaseSQL, "OBJECT_NAME, TABLE_TYPE, COMMENTS", "TABLE_TYPE", oracleListTablesOrderSQL, []any{schema, schema, schema}, constraints, ) } func oracleListSessionUserTablesQuery(constraints metadataListConstraints) oracleMetadataListQuery { return oracleConstrainedMetadataListQuery( oracleListTablesSessionUserBaseSQL, "OBJECT_NAME, TABLE_TYPE, COMMENTS", "TABLE_TYPE", oracleListTablesOrderSQL, nil, constraints, ) } func oracleListObjectsQuery(schema string, constraints metadataListConstraints) oracleMetadataListQuery { return oracleConstrainedMetadataListQuery( oracleListObjectsBaseSQL, "OBJECT_NAME, OBJECT_TYPE, COMMENTS", "OBJECT_TYPE", oracleListObjectsOrderSQL, []any{schema, schema, schema}, constraints, ) } func oracleListSessionUserObjectsQuery(constraints metadataListConstraints) oracleMetadataListQuery { return oracleConstrainedMetadataListQuery( oracleListObjectsSessionUserBaseSQL, "OBJECT_NAME, OBJECT_TYPE, COMMENTS", "OBJECT_TYPE", oracleListObjectsOrderSQL, nil, constraints, ) } func oracleConstrainedMetadataListQuery(baseSQL, selectList, typeColumn, orderSQL string, baseArgs []any, constraints metadataListConstraints) oracleMetadataListQuery { args := append([]any{}, baseArgs...) where := make([]string, 0, 2) if filter := strings.TrimSpace(constraints.Filter); filter != "" { args = append(args, strings.ToUpper(oracleFuzzyLikePattern(filter))) where = append(where, fmt.Sprintf("UPPER(OBJECT_NAME) LIKE :%d ESCAPE '\\'", len(args))) } if objectTypes := normalizedMetadataObjectTypes(constraints.ObjectTypes); len(objectTypes) > 0 { placeholders := make([]string, 0, len(objectTypes)) for _, objectType := range objectTypes { args = append(args, objectType) placeholders = append(placeholders, fmt.Sprintf(":%d", len(args))) } where = append(where, fmt.Sprintf("%s IN (%s)", typeColumn, strings.Join(placeholders, ","))) } sqlText := fmt.Sprintf("SELECT %s\nFROM (\n%s\n)", selectList, baseSQL) if len(where) > 0 { sqlText += "\nWHERE " + strings.Join(where, " AND ") } sqlText += "\n" + orderSQL if constraints.Limit > 0 { args = append(args, constraints.Offset+constraints.Limit) maxRowParam := len(args) args = append(args, constraints.Offset) offsetParam := len(args) sqlText = fmt.Sprintf( "SELECT %s\nFROM (\n SELECT DBX_Q.*, ROWNUM AS DBX_RN\n FROM (\n%s\n ) DBX_Q\n WHERE ROWNUM <= :%d\n)\nWHERE DBX_RN > :%d", selectList, sqlText, maxRowParam, offsetParam, ) } else if constraints.Offset > 0 { args = append(args, constraints.Offset) offsetParam := len(args) sqlText = fmt.Sprintf( "SELECT %s\nFROM (\n SELECT DBX_Q.*, ROWNUM AS DBX_RN\n FROM (\n%s\n ) DBX_Q\n)\nWHERE DBX_RN > :%d", selectList, sqlText, offsetParam, ) } return oracleMetadataListQuery{SQL: sqlText, Args: args} } func normalizedMetadataObjectTypes(values []string) []string { seen := map[string]bool{} result := make([]string, 0, len(values)) for _, value := range values { normalized := strings.ToUpper(strings.TrimSpace(value)) normalized = strings.ReplaceAll(normalized, "-", "_") normalized = strings.ReplaceAll(normalized, " ", "_") if normalized == "" || seen[normalized] { continue } seen[normalized] = true result = append(result, normalized) } sort.Strings(result) return result } func oracleFuzzyLikePattern(value string) string { value = strings.TrimSpace(value) if value == "" { return "%%" } var builder strings.Builder builder.Grow(len(value)*2 + 2) builder.WriteByte('%') for _, ch := range value { switch ch { case '\\', '%', '_': builder.WriteByte('\\') } builder.WriteRune(ch) builder.WriteByte('%') } return builder.String() } func (s *server) listTables(schema string, constraints metadataListConstraints) ([]tableInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } query := oracleListTablesQuery(schema, constraints) if s.schemaIsSessionUser(schema) { query = oracleListSessionUserTablesQuery(constraints) } rows, err := s.queryRows(query.SQL, query.Args) if err != nil { if isOraclePGALimitError(err) { return []tableInfo{}, nil } return nil, err } defer s.closeRows(rows) var result []tableInfo for rows.Next() { var item tableInfo if err := rows.Scan(&item.Name, &item.TableType, &item.Comment); err != nil { return nil, err } result = append(result, item) } return emptyIfNil(result), rows.Err() } func (s *server) listObjects(schema string, constraints metadataListConstraints) ([]objectInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } query := oracleListObjectsQuery(schema, constraints) if s.schemaIsSessionUser(schema) { query = oracleListSessionUserObjectsQuery(constraints) } rows, err := s.queryRows(query.SQL, query.Args) if err != nil { if isOraclePGALimitError(err) { return []objectInfo{}, nil } return nil, err } defer s.closeRows(rows) var result []objectInfo for rows.Next() { var item objectInfo item.Schema = schema if err := rows.Scan(&item.Name, &item.ObjectType, &item.Comment); err != nil { return nil, err } result = append(result, item) } return emptyIfNil(result), rows.Err() } func (s *server) completionAssistantSearch(request completionAssistantRequest) (completionAssistantResponse, error) { limit := request.MaxResults if limit <= 0 { limit = 100 } if limit > 1000 { limit = 1000 } preferredSchema := strings.ToUpper(strings.TrimSpace(request.Schema)) if preferredSchema == "" { var err error preferredSchema, err = s.currentSchema() if err != nil { return completionAssistantResponse{}, err } } if completionRequestHasTableLikeKind(request.ObjectKinds) { return s.completionAssistantTables(request, preferredSchema, limit) } if completionRequestHasRoutineKind(request.ObjectKinds) { return s.completionAssistantRoutines(request, preferredSchema, limit) } return completionAssistantResponse{Candidates: []completionAssistantCandidate{}}, nil } func completionRequestHasTableLikeKind(kinds []string) bool { if len(kinds) == 0 { return true } for _, kind := range kinds { switch strings.ToLower(strings.TrimSpace(kind)) { case "table", "view": return true } } return false } func completionRequestHasRoutineKind(kinds []string) bool { for _, kind := range kinds { switch strings.ToLower(strings.TrimSpace(kind)) { case "routine", "procedure", "function": return true } } return false } func (s *server) completionAssistantTables(request completionAssistantRequest, preferredSchema string, limit int) (completionAssistantResponse, error) { scanLimit := limit * 3 if scanLimit < limit+1 { scanLimit = limit + 1 } if scanLimit > 1000 { scanLimit = 1000 } query := oracleCompletionTablesQuery(request, preferredSchema, scanLimit+1) rows, err := s.queryRows(query.SQL, query.Args) if err != nil { return completionAssistantResponse{}, err } type tableRow struct { owner, name, objectType string targetOwner, targetName sql.NullString } rawRows := make([]tableRow, 0, scanLimit+1) targets := make([]oracleCompletionSynonymTarget, 0) for rows.Next() { var row tableRow if err := rows.Scan(&row.owner, &row.name, &row.objectType, &row.targetOwner, &row.targetName); err != nil { s.closeRows(rows) return completionAssistantResponse{}, err } rawRows = append(rawRows, row) if strings.EqualFold(row.objectType, "SYNONYM") && row.targetOwner.Valid && row.targetName.Valid { targets = append(targets, oracleCompletionSynonymTarget{Owner: row.targetOwner.String, Name: row.targetName.String}) } } rowsErr := rows.Err() closeErr := s.closeRows(rows) if rowsErr != nil { return completionAssistantResponse{}, rowsErr } if closeErr != nil { return completionAssistantResponse{}, closeErr } validTargets, err := s.oracleCompletionValidSynonymTargets(targets, oracleCompletionTableObjectTypes(request.ObjectKinds)) if err != nil { return completionAssistantResponse{}, err } candidates := make([]completionAssistantCandidate, 0, limit+1) for _, row := range rawRows { if strings.EqualFold(row.objectType, "SYNONYM") { if !row.targetOwner.Valid || !row.targetName.Valid { continue } if _, ok := validTargets[oracleCompletionSynonymTargetKey(row.targetOwner.String, row.targetName.String)]; !ok { continue } } kind := "table" if strings.EqualFold(row.objectType, "VIEW") { kind = "view" } candidates = append(candidates, completionAssistantCandidate{ Name: row.name, Kind: kind, Database: stringPointer(request.Database), Schema: stringPointer(row.owner), DataType: stringPointer(row.objectType), }) } incomplete := len(candidates) > limit || len(rawRows) > scanLimit if incomplete { if len(candidates) > limit { candidates = candidates[:limit] } } return completionAssistantResponse{Candidates: candidates, Incomplete: incomplete}, nil } type oracleCompletionSynonymTarget struct { Owner string Name string } func oracleCompletionSynonymTargetKey(owner, name string) string { return owner + "\x00" + name } func (s *server) oracleCompletionValidSynonymTargets(targets []oracleCompletionSynonymTarget, objectTypes []string) (map[string]struct{}, error) { unique := make([]oracleCompletionSynonymTarget, 0, len(targets)) seen := make(map[string]struct{}, len(targets)) for _, target := range targets { key := oracleCompletionSynonymTargetKey(target.Owner, target.Name) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} unique = append(unique, target) } valid := make(map[string]struct{}, len(unique)) const batchSize = 100 for start := 0; start < len(unique); start += batchSize { end := start + batchSize if end > len(unique) { end = len(unique) } query := oracleCompletionSynonymTargetsQuery(unique[start:end], objectTypes) rows, err := s.queryRows(query.SQL, query.Args) if err != nil { return nil, err } for rows.Next() { var owner, name string if err := rows.Scan(&owner, &name); err != nil { s.closeRows(rows) return nil, err } valid[oracleCompletionSynonymTargetKey(owner, name)] = struct{}{} } rowsErr := rows.Err() closeErr := s.closeRows(rows) if rowsErr != nil { return nil, rowsErr } if closeErr != nil { return nil, closeErr } } return valid, nil } func (s *server) completionAssistantRoutines(request completionAssistantRequest, preferredSchema string, limit int) (completionAssistantResponse, error) { if strings.TrimSpace(request.ParentName) != "" { return s.completionAssistantPackageRoutines(request, preferredSchema, limit) } query := oracleCompletionRoutinesQuery(request, preferredSchema, limit+1) rows, err := s.queryRows(query.SQL, query.Args) if err != nil { return completionAssistantResponse{}, err } defer s.closeRows(rows) candidates := make([]completionAssistantCandidate, 0, limit+1) for rows.Next() { var owner, name, objectType string var parentName sql.NullString if err := rows.Scan(&owner, &name, &objectType, &parentName); err != nil { return completionAssistantResponse{}, err } kind := strings.ToLower(objectType) if objectType == "PACKAGE" { kind = "object" } candidate := completionAssistantCandidate{ Name: name, Kind: kind, Database: stringPointer(request.Database), Schema: stringPointer(owner), DataType: stringPointer(objectType), } if parentName.Valid { candidate.ParentSchema = stringPointer(owner) candidate.ParentName = stringPointer(parentName.String) } candidates = append(candidates, candidate) } if err := rows.Err(); err != nil { return completionAssistantResponse{}, err } incomplete := len(candidates) > limit if incomplete { candidates = candidates[:limit] } return completionAssistantResponse{Candidates: candidates, Incomplete: incomplete}, nil } type oraclePackageRoutineRow struct { owner string parentName string name string objectID int64 subprogramID int64 position sql.NullInt64 sequence sql.NullInt64 argumentName sql.NullString inOut sql.NullString dataType sql.NullString typeOwner sql.NullString typeName sql.NullString typeSubname sql.NullString dataLength sql.NullInt64 dataPrecision sql.NullInt64 dataScale sql.NullInt64 } type oraclePackageRoutine struct { owner string parentName string name string isFunction bool returnType string argumentList []string } func (s *server) completionAssistantPackageRoutines(request completionAssistantRequest, preferredSchema string, limit int) (completionAssistantResponse, error) { query := oracleCompletionPackageRoutinesQuery(request, preferredSchema) rows, err := s.queryRows(query.SQL, query.Args) if err != nil { return completionAssistantResponse{}, err } defer s.closeRows(rows) packageRows := make([]oraclePackageRoutineRow, 0) for rows.Next() { var row oraclePackageRoutineRow if err := rows.Scan( &row.owner, &row.parentName, &row.name, &row.objectID, &row.subprogramID, &row.position, &row.sequence, &row.argumentName, &row.inOut, &row.dataType, &row.typeOwner, &row.typeName, &row.typeSubname, &row.dataLength, &row.dataPrecision, &row.dataScale, ); err != nil { return completionAssistantResponse{}, err } packageRows = append(packageRows, row) } if err := rows.Err(); err != nil { return completionAssistantResponse{}, err } return oracleCompletionPackageCandidates(packageRows, request.Database, limit), nil } func oracleCompletionPackageCandidates(rows []oraclePackageRoutineRow, database string, limit int) completionAssistantResponse { routines := make([]oraclePackageRoutine, 0) routineIndexes := make(map[string]int) for _, row := range rows { key := fmt.Sprintf("%s\x00%d\x00%d", row.owner, row.objectID, row.subprogramID) index, found := routineIndexes[key] if !found { index = len(routines) routineIndexes[key] = index routines = append(routines, oraclePackageRoutine{owner: row.owner, parentName: row.parentName, name: row.name}) } routine := &routines[index] if !row.position.Valid { continue } dataType := oracleCompletionArgumentDataType(row) if row.position.Int64 == 0 { routine.isFunction = true routine.returnType = dataType continue } argument := oracleCompletionArgumentSignature(row, dataType) if argument != "" { routine.argumentList = append(routine.argumentList, argument) } } incomplete := len(routines) > limit if incomplete { routines = routines[:limit] } candidates := make([]completionAssistantCandidate, 0, len(routines)) for _, routine := range routines { kind := "procedure" if routine.isFunction { kind = "function" } candidate := completionAssistantCandidate{ Name: routine.name, Kind: kind, Database: stringPointer(database), Schema: stringPointer(routine.owner), ParentSchema: stringPointer(routine.owner), ParentName: stringPointer(routine.parentName), Signature: stringPointer(strings.Join(routine.argumentList, ", ")), } if routine.returnType != "" { candidate.DataType = stringPointer(routine.returnType) } candidates = append(candidates, candidate) } return completionAssistantResponse{Candidates: candidates, Incomplete: incomplete} } func oracleCompletionArgumentSignature(row oraclePackageRoutineRow, dataType string) string { parts := make([]string, 0, 3) if row.argumentName.Valid { parts = append(parts, strings.TrimSpace(row.argumentName.String)) } if row.inOut.Valid { direction := strings.Join(strings.Fields(strings.ReplaceAll(row.inOut.String, "/", " ")), " ") if direction != "" { parts = append(parts, direction) } } if dataType != "" { parts = append(parts, dataType) } return strings.Join(parts, " ") } func oracleCompletionArgumentDataType(row oraclePackageRoutineRow) string { typeParts := make([]string, 0, 3) if row.typeOwner.Valid && strings.TrimSpace(row.typeOwner.String) != "" { typeParts = append(typeParts, strings.TrimSpace(row.typeOwner.String)) } if row.typeName.Valid && strings.TrimSpace(row.typeName.String) != "" { typeParts = append(typeParts, strings.TrimSpace(row.typeName.String)) } if row.typeSubname.Valid && strings.TrimSpace(row.typeSubname.String) != "" { typeParts = append(typeParts, strings.TrimSpace(row.typeSubname.String)) } if len(typeParts) > 0 { return strings.Join(typeParts, ".") } dataType := strings.TrimSpace(row.dataType.String) switch strings.ToUpper(dataType) { case "NUMBER", "NUMERIC", "DECIMAL": if row.dataPrecision.Valid { if row.dataScale.Valid { return fmt.Sprintf("%s(%d, %d)", dataType, row.dataPrecision.Int64, row.dataScale.Int64) } return fmt.Sprintf("%s(%d)", dataType, row.dataPrecision.Int64) } case "CHAR", "VARCHAR", "VARCHAR2", "NCHAR", "NVARCHAR2", "RAW": if row.dataLength.Valid && row.dataLength.Int64 > 0 { return fmt.Sprintf("%s(%d)", dataType, row.dataLength.Int64) } } return dataType } func stringPointer(value string) *string { if value == "" { return nil } result := value return &result } func oracleCompletionTableObjectTypes(kinds []string) []string { objectTypes := make([]string, 0, 2) for _, kind := range kinds { switch strings.ToLower(strings.TrimSpace(kind)) { case "table": objectTypes = append(objectTypes, "'TABLE'") case "view": objectTypes = append(objectTypes, "'VIEW'") } } if len(objectTypes) == 0 { objectTypes = []string{"'TABLE'", "'VIEW'"} } return objectTypes } func oracleCompletionTablesQuery(request completionAssistantRequest, preferredSchema string, limit int) oracleMetadataListQuery { objectTypes := oracleCompletionTableObjectTypes(request.ObjectKinds) pattern := oracleCompletionLikePattern(request.Mask, request.MatchMode) args := make([]any, 0, 7) args = append(args, pattern) objectNamePredicate := oracleCompletionNamePredicate("o.OBJECT_NAME", len(args), request.CaseSensitive) objectOwnerPredicate := "" synonymOwnerPredicate := "" owner := "" if !request.GlobalSearch { owner = strings.ToUpper(strings.TrimSpace(request.ParentSchema)) if owner == "" { owner = strings.ToUpper(strings.TrimSpace(request.Schema)) } if owner == "" { owner = preferredSchema } args = append(args, owner) objectOwnerPredicate = fmt.Sprintf(" AND o.OWNER = :%d", len(args)) } args = append(args, pattern) synonymNamePredicate := oracleCompletionNamePredicate("s.SYNONYM_NAME", len(args), request.CaseSensitive) if owner != "" { args = append(args, owner) synonymOwnerPredicate = fmt.Sprintf(" AND s.OWNER = :%d", len(args)) } args = append(args, preferredSchema) preferredParam := len(args) args = append(args, strings.TrimSpace(request.Mask)) exactParam := len(args) args = append(args, limit) limitParam := len(args) baseSQL := fmt.Sprintf(`SELECT o.OWNER, o.OBJECT_NAME, o.OBJECT_TYPE, CAST(NULL AS VARCHAR2(128)) AS TARGET_OWNER, CAST(NULL AS VARCHAR2(128)) AS TARGET_NAME FROM ALL_OBJECTS o WHERE o.OBJECT_TYPE IN (%s) AND %s%s UNION ALL SELECT s.OWNER, s.SYNONYM_NAME AS OBJECT_NAME, 'SYNONYM' AS OBJECT_TYPE, s.TABLE_OWNER AS TARGET_OWNER, s.TABLE_NAME AS TARGET_NAME FROM ALL_SYNONYMS s WHERE s.DB_LINK IS NULL AND %s%s`, strings.Join(objectTypes, ","), objectNamePredicate, objectOwnerPredicate, synonymNamePredicate, synonymOwnerPredicate) unionSQL := fmt.Sprintf("SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, TARGET_OWNER, TARGET_NAME\nFROM (\n%s\n)", baseSQL) orderedSQL := oracleCompletionOrderedSQL(unionSQL, "OBJECT_NAME", "OBJECT_TYPE", preferredParam, exactParam) return oracleMetadataListQuery{ SQL: fmt.Sprintf("SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, TARGET_OWNER, TARGET_NAME FROM (\n%s\n) WHERE ROWNUM <= :%d", orderedSQL, limitParam), Args: args, } } func oracleCompletionSynonymTargetsQuery(targets []oracleCompletionSynonymTarget, objectTypes []string) oracleMetadataListQuery { args := make([]any, 0, len(targets)*2) predicates := make([]string, 0, len(targets)) for _, target := range targets { args = append(args, target.Owner, target.Name) predicates = append(predicates, fmt.Sprintf("(o.OWNER = :%d AND o.OBJECT_NAME = :%d)", len(args)-1, len(args))) } return oracleMetadataListQuery{ SQL: fmt.Sprintf("SELECT DISTINCT o.OWNER, o.OBJECT_NAME\nFROM ALL_OBJECTS o\nWHERE o.OBJECT_TYPE IN (%s)\n AND (%s)", strings.Join(objectTypes, ","), strings.Join(predicates, " OR ")), Args: args, } } func oracleCompletionRoutinesQuery(request completionAssistantRequest, preferredSchema string, limit int) oracleMetadataListQuery { pattern := oracleCompletionLikePattern(request.Mask, request.MatchMode) args := make([]any, 0, 5) baseSQL := ` SELECT o.OWNER, o.OBJECT_NAME, o.OBJECT_TYPE, CAST(NULL AS VARCHAR2(128)) AS PARENT_NAME FROM ALL_OBJECTS o WHERE o.OBJECT_TYPE IN ('FUNCTION', 'PROCEDURE', 'PACKAGE')` args = append(args, pattern) nameParam := len(args) ownerPredicate := "" if !request.GlobalSearch { owner := strings.ToUpper(strings.TrimSpace(request.ParentSchema)) if owner == "" { owner = strings.ToUpper(strings.TrimSpace(request.Schema)) } if owner == "" { owner = preferredSchema } args = append(args, owner) ownerPredicate = fmt.Sprintf(" AND OWNER = :%d", len(args)) } args = append(args, preferredSchema) preferredParam := len(args) args = append(args, strings.TrimSpace(request.Mask)) exactParam := len(args) args = append(args, limit) limitParam := len(args) filteredSQL := fmt.Sprintf("SELECT DISTINCT OWNER, OBJECT_NAME, OBJECT_TYPE, PARENT_NAME\nFROM (\n%s\n)\nWHERE %s%s", baseSQL, oracleCompletionNamePredicate("OBJECT_NAME", nameParam, request.CaseSensitive), ownerPredicate) orderedSQL := oracleCompletionOrderedSQL(filteredSQL, "OBJECT_NAME", "OBJECT_TYPE", preferredParam, exactParam) return oracleMetadataListQuery{ SQL: fmt.Sprintf("SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, PARENT_NAME FROM (\n%s\n) WHERE ROWNUM <= :%d", orderedSQL, limitParam), Args: args, } } func oracleCompletionPackageRoutinesQuery(request completionAssistantRequest, preferredSchema string) oracleMetadataListQuery { owner := strings.TrimSpace(request.ParentSchema) if owner == "" { owner = strings.TrimSpace(request.Schema) } if owner == "" { owner = preferredSchema } parentName := strings.TrimSpace(request.ParentName) pattern := oracleCompletionLikePattern(request.Mask, request.MatchMode) args := []any{owner, parentName, pattern} return oracleMetadataListQuery{ SQL: fmt.Sprintf(`SELECT p.OWNER, p.OBJECT_NAME AS PARENT_NAME, p.PROCEDURE_NAME AS OBJECT_NAME, p.OBJECT_ID, p.SUBPROGRAM_ID, a.POSITION, a.SEQUENCE, a.ARGUMENT_NAME, a.IN_OUT, a.DATA_TYPE, a.TYPE_OWNER, a.TYPE_NAME, a.TYPE_SUBNAME, a.DATA_LENGTH, a.DATA_PRECISION, a.DATA_SCALE FROM ALL_PROCEDURES p LEFT JOIN ALL_ARGUMENTS a ON a.OWNER = p.OWNER AND a.OBJECT_ID = p.OBJECT_ID AND a.SUBPROGRAM_ID = p.SUBPROGRAM_ID AND a.DATA_LEVEL = 0 WHERE p.OBJECT_TYPE = 'PACKAGE' AND p.PROCEDURE_NAME IS NOT NULL AND p.OWNER = :1 AND p.OBJECT_NAME = :2 AND %s ORDER BY p.PROCEDURE_NAME, p.SUBPROGRAM_ID, NVL(a.SEQUENCE, 0)`, oracleCompletionNamePredicate("p.PROCEDURE_NAME", 3, request.CaseSensitive)), Args: args, } } func oracleCompletionNamePredicate(column string, parameter int, caseSensitive bool) string { if caseSensitive { return fmt.Sprintf("%s LIKE :%d ESCAPE '\\'", column, parameter) } return fmt.Sprintf("UPPER(%s) LIKE UPPER(:%d) ESCAPE '\\'", column, parameter) } func oracleCompletionLikePattern(mask, matchMode string) string { escaped := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(strings.TrimSpace(mask)) if strings.EqualFold(strings.TrimSpace(matchMode), "contains") { return "%" + escaped + "%" } return escaped + "%" } func oracleCompletionOrderedSQL(baseSQL, nameColumn, typeColumn string, preferredParam, exactParam int) string { return fmt.Sprintf(`%s ORDER BY CASE WHEN OWNER = :%d THEN 0 WHEN OWNER = 'PUBLIC' THEN 1 WHEN OWNER IN ('SYS','SYSTEM','SYSMAN','DBSNMP','OUTLN','XDB','MDSYS','CTXSYS','WMSYS') THEN 3 ELSE 2 END, CASE WHEN UPPER(%s) = UPPER(:%d) THEN 0 ELSE 1 END, CASE %s WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 WHEN 'FUNCTION' THEN 0 WHEN 'PROCEDURE' THEN 1 WHEN 'PACKAGE' THEN 2 WHEN 'SYNONYM' THEN 3 ELSE 4 END, %s, OWNER`, baseSQL, preferredParam, nameColumn, exactParam, typeColumn, nameColumn) } func oracleObjectNameCandidates(name string) (string, string, bool) { exact := strings.TrimSpace(name) uppercase := strings.ToUpper(exact) return exact, uppercase, exact != uppercase } func (s *server) getColumns(schema, table string) ([]columnInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(table) result, err := s.getColumnsByName(schema, exact) if err != nil || len(result) > 0 || !hasUppercaseFallback { return result, err } return s.getColumnsByName(schema, uppercase) } func (s *server) getColumnsByName(schema, table string) ([]columnInfo, error) { rows, err := s.queryRows(` SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_DEFAULT, CASE WHEN pk.COLUMN_NAME IS NULL THEN 0 ELSE 1 END AS IS_PRIMARY_KEY, cc.COMMENTS, c.DATA_PRECISION, c.DATA_SCALE, c.CHAR_LENGTH, c.CHAR_USED FROM ALL_TAB_COLUMNS c LEFT JOIN ( SELECT acc.OWNER, acc.TABLE_NAME, acc.COLUMN_NAME FROM ALL_CONSTRAINTS ac JOIN ALL_CONS_COLUMNS acc ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME WHERE ac.CONSTRAINT_TYPE = 'P' ) pk ON pk.OWNER = c.OWNER AND pk.TABLE_NAME = c.TABLE_NAME AND pk.COLUMN_NAME = c.COLUMN_NAME LEFT JOIN ALL_COL_COMMENTS cc ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME WHERE c.OWNER = :1 AND c.TABLE_NAME = :2 ORDER BY c.COLUMN_ID`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) var result []columnInfo for rows.Next() { var item columnInfo var nullable string var primary int if err := rows.Scan( &item.Name, &item.DataType, &nullable, &item.ColumnDefault, &primary, &item.Comment, &item.NumericPrecision, &item.NumericScale, &item.CharacterMaximumLength, &item.CharacterLengthUnit, ); err != nil { return nil, err } item.IsNullable = nullable == "Y" item.IsPrimaryKey = primary != 0 item.DataType = oracleColumnTypeDDL(item) result = append(result, item) } return emptyIfNil(result), rows.Err() } func (s *server) getTableComment(schema, table string) (*string, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(table) comment, found, err := s.getTableCommentByName(schema, exact) if err != nil || found || !hasUppercaseFallback { return comment, err } comment, _, err = s.getTableCommentByName(schema, uppercase) return comment, err } func (s *server) getTableCommentByName(schema, table string) (*string, bool, error) { db, err := s.requireDB() if err != nil { return nil, false, err } var comment sql.NullString err = db.QueryRow( "SELECT COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = :1 AND TABLE_NAME = :2", schema, table, ).Scan(&comment) if errors.Is(err, sql.ErrNoRows) { return nil, false, nil } if err != nil { return nil, false, err } if !comment.Valid { return nil, true, nil } return &comment.String, true, nil } func (s *server) loadOracleColumnMeta(schema, table string) ([]oracleColumnMeta, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(table) result, err := s.loadOracleColumnMetaByName(schema, exact) if err != nil || len(result) > 0 || !hasUppercaseFallback { return result, err } return s.loadOracleColumnMetaByName(schema, uppercase) } func (s *server) loadOracleColumnMetaByName(schema, table string) ([]oracleColumnMeta, error) { rows, err := s.queryRows(` SELECT COLUMN_NAME, DATA_TYPE FROM ALL_TAB_COLUMNS WHERE OWNER = :1 AND TABLE_NAME = :2 ORDER BY COLUMN_ID`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) var result []oracleColumnMeta for rows.Next() { var item oracleColumnMeta if err := rows.Scan(&item.Name, &item.DataType); err != nil { return nil, err } result = append(result, item) } return result, rows.Err() } func (s *server) listIndexes(schema, table string) ([]indexInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } table = strings.TrimSpace(table) rows, err := s.queryRows(` SELECT i.INDEX_NAME, ic.COLUMN_NAME, i.UNIQUENESS, CASE WHEN pk.CONSTRAINT_NAME IS NULL THEN 0 ELSE 1 END AS IS_PRIMARY, i.INDEX_TYPE, ic.COLUMN_POSITION FROM ALL_INDEXES i JOIN ALL_IND_COLUMNS ic ON ic.INDEX_OWNER = i.OWNER AND ic.INDEX_NAME = i.INDEX_NAME LEFT JOIN ALL_CONSTRAINTS pk ON pk.OWNER = i.TABLE_OWNER AND pk.TABLE_NAME = i.TABLE_NAME AND pk.CONSTRAINT_TYPE = 'P' AND pk.INDEX_NAME = i.INDEX_NAME WHERE i.TABLE_OWNER = :1 AND i.TABLE_NAME = :2 ORDER BY i.INDEX_NAME, ic.COLUMN_POSITION`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) byName := map[string]*indexInfo{} order := []string{} for rows.Next() { var name, column, uniqueness, indexType string var primary int var position int if err := rows.Scan(&name, &column, &uniqueness, &primary, &indexType, &position); err != nil { return nil, err } item := byName[name] if item == nil { item = &indexInfo{ Name: name, Columns: []string{}, IsUnique: uniqueness == "UNIQUE", IsPrimary: primary != 0, IndexType: &indexType, IncludedColumns: []string{}, } byName[name] = item order = append(order, name) } item.Columns = append(item.Columns, column) } if err := rows.Err(); err != nil { return nil, err } result := make([]indexInfo, 0, len(order)) for _, name := range order { result = append(result, *byName[name]) } return emptyIfNil(result), nil } func (s *server) listForeignKeys(schema, table string) ([]foreignKeyInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } table = strings.TrimSpace(table) rows, err := s.queryRows(` SELECT ac.CONSTRAINT_NAME, acc.COLUMN_NAME, rcc.OWNER AS REF_SCHEMA, rcc.TABLE_NAME AS REF_TABLE, rcc.COLUMN_NAME AS REF_COLUMN, ac.DELETE_RULE FROM ALL_CONSTRAINTS ac JOIN ALL_CONS_COLUMNS acc ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME JOIN ALL_CONS_COLUMNS rcc ON rcc.OWNER = ac.R_OWNER AND rcc.CONSTRAINT_NAME = ac.R_CONSTRAINT_NAME AND rcc.POSITION = acc.POSITION WHERE ac.OWNER = :1 AND ac.TABLE_NAME = :2 AND ac.CONSTRAINT_TYPE = 'R' ORDER BY ac.CONSTRAINT_NAME, acc.POSITION`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) var result []foreignKeyInfo for rows.Next() { var item foreignKeyInfo if err := rows.Scan(&item.Name, &item.Column, &item.RefSchema, &item.RefTable, &item.RefColumn, &item.OnDelete); err != nil { return nil, err } result = append(result, item) } return emptyIfNil(result), rows.Err() } func oracleConstraintTypeName(kind string) string { switch kind { case "P": return "PRIMARY KEY" case "U": return "UNIQUE" case "C": return "CHECK" default: return kind } } func oracleSystemNotNullConstraint(kind string, generated sql.NullString, definition string, column, nullable sql.NullString) bool { if kind != "C" || !generated.Valid || generated.String != "GENERATED NAME" || !column.Valid || !nullable.Valid || nullable.String != "N" { return false } matches := oracleNotNullConstraintRegexp.FindStringSubmatch(definition) if matches == nil { return false } if matches[1] != "" { return strings.ReplaceAll(matches[1], `""`, `"`) == column.String } return strings.EqualFold(matches[2], column.String) } // listConstraints returns primary key, unique, and check constraints for a // table. Oracle represents every NOT NULL column as a system-generated CHECK // constraint (e.g. "COL" IS NOT NULL); those are excluded here so the result // only contains constraints a user would recognize as such, matching how // tools like DBeaver/Navicat present Oracle constraints. func (s *server) listConstraints(schema, table string) ([]constraintInfo, error) { schema, err := s.normalizeSchemaForIdentity(schema) if err != nil { return nil, err } table = strings.TrimSpace(table) // SEARCH_CONDITION is a LONG column: Oracle rejects LONG values in WHERE // clauses, functions, or ORDER BY (ORA-00932), so it can only appear in // the SELECT list here. The NOT-NULL-check exclusion below is therefore // applied in Go after scanning, not in SQL. rows, err := s.queryRows(` SELECT ac.CONSTRAINT_NAME, ac.CONSTRAINT_TYPE, ac.SEARCH_CONDITION, ac.GENERATED, ac.STATUS, ac.DEFERRABLE, ac.DEFERRED, ac.VALIDATED, acc.COLUMN_NAME, acc.POSITION, atc.NULLABLE FROM ALL_CONSTRAINTS ac LEFT JOIN ALL_CONS_COLUMNS acc ON acc.OWNER = ac.OWNER AND acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME LEFT JOIN ALL_TAB_COLUMNS atc ON atc.OWNER = ac.OWNER AND atc.TABLE_NAME = ac.TABLE_NAME AND atc.COLUMN_NAME = acc.COLUMN_NAME WHERE ac.OWNER = :1 AND ac.TABLE_NAME = :2 AND ac.CONSTRAINT_TYPE IN ('P', 'U', 'C') ORDER BY ac.CONSTRAINT_NAME, acc.POSITION`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) byName := map[string]*constraintInfo{} skipped := map[string]bool{} order := []string{} for rows.Next() { var name, kind string var condition, generated, status, deferrable, deferred, validated, column, nullable sql.NullString var position sql.NullInt64 if err := rows.Scan(&name, &kind, &condition, &generated, &status, &deferrable, &deferred, &validated, &column, &position, &nullable); err != nil { return nil, err } if skipped[name] { continue } item := byName[name] if item == nil { definition := "" if condition.Valid { definition = strings.TrimSpace(condition.String) } if oracleSystemNotNullConstraint(kind, generated, definition, column, nullable) { skipped[name] = true continue } item = &constraintInfo{ Name: name, ConstraintType: oracleConstraintTypeName(kind), Definition: definition, Columns: []string{}, RefColumns: []string{}, Deferrable: deferrable.Valid && deferrable.String == "DEFERRABLE", InitiallyDeferred: deferred.Valid && deferred.String == "DEFERRED", Enabled: status.Valid && status.String == "ENABLED", Valid: validated.Valid && validated.String == "VALIDATED", } byName[name] = item order = append(order, name) } if column.Valid && column.String != "" { item.Columns = append(item.Columns, column.String) } } if err := rows.Err(); err != nil { return nil, err } result := make([]constraintInfo, 0, len(order)) for _, name := range order { result = append(result, *byName[name]) } return emptyIfNil(result), nil } func (s *server) listTriggers(schema, table string) ([]triggerInfo, error) { schema, err := s.normalizeSchema(schema) if err != nil { return nil, err } table = strings.TrimSpace(table) rows, err := s.queryRows(oracleListTriggersSQL, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) var result []triggerInfo var currentName string var currentDescription string var source strings.Builder flush := func() { if len(result) == 0 || currentName == "" { return } if body, ok := oracleTriggerBody(source.String(), currentDescription); ok { result[len(result)-1].Statement = &body } } for rows.Next() { var name, event, timing string var description, lineText sql.NullString var line sql.NullInt64 if err := rows.Scan(&name, &event, &timing, &description, &line, &lineText); err != nil { return nil, err } if name != currentName { flush() currentName = name currentDescription = description.String source.Reset() result = append(result, triggerInfo{Name: name, Event: event, Timing: timing}) } if line.Valid && lineText.Valid { source.WriteString(lineText.String) } } flush() return emptyIfNil(result), rows.Err() } func oracleTriggerBody(source, description string) (string, bool) { source = strings.ReplaceAll(source, "\r\n", "\n") description = strings.ReplaceAll(description, "\r\n", "\n") if strings.TrimSpace(source) == "" { return "", false } sourceLines := strings.Split(source, "\n") descriptionLines := strings.Split(strings.TrimSpace(description), "\n") for len(descriptionLines) > 0 && strings.TrimSpace(descriptionLines[len(descriptionLines)-1]) == "" { descriptionLines = descriptionLines[:len(descriptionLines)-1] } if len(descriptionLines) > 0 && len(sourceLines) >= len(descriptionLines) { matches := true for index, descriptionLine := range descriptionLines { sourceLine := strings.TrimSpace(sourceLines[index]) if index == 0 && len(sourceLine) >= len("TRIGGER") && strings.EqualFold(sourceLine[:len("TRIGGER")], "TRIGGER") { sourceLine = strings.TrimSpace(sourceLine[len("TRIGGER"):]) } if !strings.EqualFold(sourceLine, strings.TrimSpace(descriptionLine)) { matches = false break } } if matches { return strings.TrimSpace(strings.Join(sourceLines[len(descriptionLines):], "\n")), true } } // ALL_SOURCE is still more useful than an empty editor if a database version formats DESCRIPTION differently. return strings.TrimSpace(source), true } func (s *server) getObjectSource(schema, name, objectType string) (map[string]any, error) { var err error schema, err = s.normalizeSchemaForIdentity(schema) if err != nil { return nil, err } upperType := strings.ToUpper(objectType) if upperType == "VIEW" { source, err := s.getViewSource(schema, name) if err != nil { return nil, err } return map[string]any{"name": name, "object_type": objectType, "schema": schema, "source": source}, nil } if upperType == "SEQUENCE" || upperType == "SYNONYM" { return s.getMetadataObjectSource(schema, name, upperType) } // Unquoted Oracle identifiers are stored uppercase; quoted mixed-case names must stay exact. // Try caller-provided identity first, then uppercase fallback (same pattern as column metadata). for _, candidate := range oracleObjectIdentityNameCandidates(name) { source, found, queryErr := s.loadObjectSourceText(schema, candidate, upperType) if queryErr != nil { return nil, queryErr } if found { return map[string]any{"name": candidate, "object_type": objectType, "schema": schema, "source": source}, nil } } return map[string]any{"name": name, "object_type": objectType, "schema": schema, "source": ""}, nil } func (s *server) getMetadataObjectSource(schema, name, objectType string) (map[string]any, error) { db, err := s.requireDB() if err != nil { return nil, err } var lastErr error for _, candidate := range oracleObjectIdentityNameCandidates(name) { var source string lastErr = db.QueryRow("SELECT DBMS_METADATA.GET_DDL(:1, :2, :3) FROM DUAL", objectType, candidate, schema).Scan(&source) if lastErr == nil { return map[string]any{"name": candidate, "object_type": objectType, "schema": schema, "source": source}, nil } } return nil, lastErr } func (s *server) loadObjectSourceText(schema, name, objectType string) (string, bool, error) { source, found, err := s.loadAggregatedObjectSourceText(schema, name, objectType) if err == nil { return source, found, nil } return s.loadObjectSourceTextRows(schema, name, objectType) } func (s *server) loadAggregatedObjectSourceText(schema, name, objectType string) (string, bool, error) { rows, err := s.queryRows(` SELECT DBMS_XMLGEN.CONVERT( XMLAGG(XMLELEMENT(E, TEXT) ORDER BY LINE).EXTRACT('//text()').GETCLOBVAL(), 1 ) FROM ALL_SOURCE WHERE OWNER = :1 AND NAME = :2 AND TYPE = :3`, []any{schema, name, objectType}) if err != nil { return "", false, err } defer s.closeRows(rows) if !rows.Next() { return "", false, rows.Err() } var source sql.NullString if err := rows.Scan(&source); err != nil { return "", false, err } if err := rows.Err(); err != nil { return "", false, err } return source.String, source.Valid, nil } func (s *server) loadObjectSourceTextRows(schema, name, objectType string) (string, bool, error) { rows, err := s.queryRows(` SELECT TEXT FROM ALL_SOURCE WHERE OWNER = :1 AND NAME = :2 AND TYPE = :3 ORDER BY LINE`, []any{schema, name, objectType}) if err != nil { return "", false, err } defer s.closeRows(rows) var builder strings.Builder var anyLine bool for rows.Next() { var line string if err := rows.Scan(&line); err != nil { return "", false, err } builder.WriteString(line) anyLine = true } if err := rows.Err(); err != nil { return "", false, err } return builder.String(), anyLine, nil } // oracleObjectIdentityNameCandidates returns ALL_SOURCE name variants. // Exact form first (quoted mixed-case), then uppercase for unquoted identifiers. func oracleObjectIdentityNameCandidates(name string) []string { trimmed := strings.TrimSpace(name) if trimmed == "" { return nil } upper := strings.ToUpper(trimmed) if trimmed == upper { return []string{upper} } return []string{trimmed, upper} } // normalizeSchemaForIdentity preserves mixed-case schema owners (quoted identities) // and uppercases already-uppercase / empty-resolved session schemas. func (s *server) normalizeSchemaForIdentity(schema string) (string, error) { trimmed := strings.TrimSpace(schema) if trimmed != "" && trimmed != strings.ToUpper(trimmed) { // Mixed or lower case from a quoted click-site identity — keep exact OWNER. return trimmed, nil } return s.normalizeSchema(schema) } func (s *server) getTableDDL(schema, table, objectType string) (string, error) { return s.getTableDDLWithOptions(schema, table, objectType, false) } func (s *server) getTableDDLWithOptions(schema, table, objectType string, portable bool) (string, error) { var err error schema, err = s.normalizeSchema(schema) if err != nil { return "", err } db, err := s.requireDB() if err != nil { return "", err } objectType, table, err = s.resolveDDLObject(schema, table, objectType) if err != nil { return "", err } if objectType == "VIEW" { return s.buildViewDDL(schema, table) } var ddl string var indexDDLs []string if portable && objectType == "TABLE" { var metadataErr error err = withOraclePortableMetadataSession(db, func(conn *sql.Conn) error { metadataErr = conn.QueryRowContext( context.Background(), "SELECT DBMS_METADATA.GET_DDL(:1, :2, :3) FROM DUAL", objectType, table, schema, ).Scan(&ddl) indexDDLs, _ = loadTableIndexDDLsFromConn(conn, schema, table) return nil }) if err != nil { return "", err } err = metadataErr } else { err = db.QueryRow("SELECT DBMS_METADATA.GET_DDL(:1, :2, :3) FROM DUAL", objectType, table, schema).Scan(&ddl) } if err == nil && strings.TrimSpace(ddl) != "" { if objectType == "TABLE" { if portable { return s.appendTableDependentDDLWithIndexes(schema, table, ddl, indexDDLs), nil } return s.appendTableDependentDDL(schema, table, ddl), nil } return ddl, nil } if objectType == "TABLE" { fallback, fallbackErr := s.buildTableDDL(schema, table) if fallbackErr != nil { return "", fallbackErr } if portable { return s.appendTableDependentDDLWithIndexes(schema, table, fallback, indexDDLs), nil } return s.appendTableDependentDDL(schema, table, fallback), nil } return "", err } func withOraclePortableMetadataSession(db *sql.DB, operation func(*sql.Conn) error) (err error) { conn, err := db.Conn(context.Background()) if err != nil { return err } if _, err = conn.ExecContext(context.Background(), oracleDisableSegmentAttributesSQL); err != nil { _ = conn.Close() return fmt.Errorf("failed to disable Oracle segment attributes: %w", err) } defer func() { if _, resetErr := conn.ExecContext(context.Background(), oracleEnableSegmentAttributesSQL); resetErr != nil { _ = conn.Raw(func(any) error { return driver.ErrBadConn }) if err == nil { err = fmt.Errorf("failed to restore Oracle segment attributes: %w", resetErr) } } _ = conn.Close() }() return operation(conn) } func (s *server) appendTableDependentDDL(schema, table, tableDDL string) string { indexDDLs, _ := s.loadTableIndexDDLs(schema, table) return s.appendTableDependentDDLWithIndexes(schema, table, tableDDL, indexDDLs) } func (s *server) appendTableDependentDDLWithIndexes(schema, table, tableDDL string, indexDDLs []string) string { var builder strings.Builder baseDDL := strings.TrimSpace(tableDDL) builder.WriteString(baseDDL) dependentAppended := false appendDependent := func(ddl string) { if strings.TrimSpace(ddl) == "" { return } if !dependentAppended && !strings.HasSuffix(baseDDL, ";") && !strings.HasSuffix(baseDDL, "/") { builder.WriteByte(';') } appendOracleDDLFragment(&builder, ddl) dependentAppended = true } for _, ddl := range indexDDLs { appendDependent(ddl) } if triggerDDLs, err := s.loadTableTriggerDDLs(schema, table); err == nil { for _, ddl := range triggerDDLs { appendDependent(ddl) } } if comments, err := s.loadTableCommentDDLs(schema, table); err == nil { for _, ddl := range comments { appendDependent(ddl) } } return builder.String() } const oracleTableIndexDDLsSQL = ` SELECT DBMS_METADATA.GET_DDL('INDEX', i.INDEX_NAME, i.OWNER) FROM ALL_INDEXES i WHERE i.TABLE_OWNER = :1 AND i.TABLE_NAME = :2 AND i.GENERATED = 'N' AND i.INDEX_NAME NOT IN ( SELECT c.INDEX_NAME FROM ALL_CONSTRAINTS c WHERE c.OWNER = :3 AND c.TABLE_NAME = :4 AND c.CONSTRAINT_TYPE IN ('P', 'U') AND c.INDEX_NAME IS NOT NULL ) ORDER BY i.INDEX_NAME` func (s *server) loadTableIndexDDLs(schema, table string) ([]string, error) { rows, err := s.queryRows(oracleTableIndexDDLsSQL, []any{schema, table, schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) return scanOracleDDLs(rows) } func loadTableIndexDDLsFromConn(conn *sql.Conn, schema, table string) ([]string, error) { rows, err := conn.QueryContext(context.Background(), oracleTableIndexDDLsSQL, schema, table, schema, table) if err != nil { return nil, err } defer rows.Close() return scanOracleDDLs(rows) } func scanOracleDDLs(rows *sql.Rows) ([]string, error) { var result []string for rows.Next() { var ddl sql.NullString if err := rows.Scan(&ddl); err != nil { return nil, err } if ddl.Valid && strings.TrimSpace(ddl.String) != "" { result = append(result, ddl.String) } } return result, rows.Err() } func (s *server) loadTableTriggerDDLs(schema, table string) ([]string, error) { rows, err := s.queryRows(` SELECT DBMS_METADATA.GET_DDL('TRIGGER', t.TRIGGER_NAME, t.OWNER) FROM ALL_TRIGGERS t WHERE t.TABLE_OWNER = :1 AND t.TABLE_NAME = :2 ORDER BY t.TRIGGER_NAME`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) var result []string for rows.Next() { var ddl sql.NullString if err := rows.Scan(&ddl); err != nil { return nil, err } if ddl.Valid && strings.TrimSpace(ddl.String) != "" { result = append(result, ddl.String) } } return result, rows.Err() } func (s *server) loadTableCommentDDLs(schema, table string) ([]string, error) { db, err := s.requireDB() if err != nil { return nil, err } qualifiedTable := quoteIdentifier(schema) + "." + quoteIdentifier(table) var result []string var tableComment sql.NullString err = db.QueryRow( "SELECT COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = :1 AND TABLE_NAME = :2", schema, table, ).Scan(&tableComment) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } if tableComment.Valid && strings.TrimSpace(tableComment.String) != "" { result = append(result, fmt.Sprintf("COMMENT ON TABLE %s IS %s", qualifiedTable, oracleStringLiteral(tableComment.String))) } rows, err := s.queryRows(` SELECT COLUMN_NAME, COMMENTS FROM ALL_COL_COMMENTS WHERE OWNER = :1 AND TABLE_NAME = :2 AND COMMENTS IS NOT NULL ORDER BY COLUMN_NAME`, []any{schema, table}) if err != nil { return nil, err } defer s.closeRows(rows) for rows.Next() { var columnName string var comment sql.NullString if err := rows.Scan(&columnName, &comment); err != nil { return nil, err } if comment.Valid && strings.TrimSpace(comment.String) != "" { result = append(result, fmt.Sprintf( "COMMENT ON COLUMN %s.%s IS %s", qualifiedTable, quoteIdentifier(columnName), oracleStringLiteral(comment.String), )) } } return result, rows.Err() } func appendOracleDDLFragment(builder *strings.Builder, ddl string) { trimmed := strings.TrimSpace(ddl) if trimmed == "" { return } if builder.Len() > 0 { builder.WriteString("\n\n") } builder.WriteString(trimmed) if !strings.HasSuffix(trimmed, ";") && !strings.HasSuffix(trimmed, "/") { builder.WriteByte(';') } } func oracleStringLiteral(value string) string { return "'" + strings.ReplaceAll(value, "'", "''") + "'" } func (s *server) resolveDDLObject(schema, name, requested string) (string, string, error) { exact, uppercase, hasUppercaseFallback := oracleObjectNameCandidates(name) objectType := normalizeDDLObjectType(requested) if objectType != "" { return objectType, exact, nil } db, err := s.requireDB() if err != nil { return "", "", err } resolve := func(objectName string) error { return db.QueryRow(` SELECT OBJECT_TYPE FROM ( SELECT OBJECT_TYPE FROM ALL_OBJECTS WHERE OWNER = :1 AND OBJECT_NAME = :2 AND OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW') ORDER BY CASE OBJECT_TYPE WHEN 'TABLE' THEN 0 WHEN 'VIEW' THEN 1 ELSE 2 END ) WHERE ROWNUM = 1`, schema, objectName).Scan(&objectType) } err = resolve(exact) if errors.Is(err, sql.ErrNoRows) && hasUppercaseFallback { err = resolve(uppercase) if err == nil { exact = uppercase } } if errors.Is(err, sql.ErrNoRows) { return "", "", fmt.Errorf("object not found: %s.%s", schema, name) } if err != nil { return "", "", err } return normalizeDDLObjectType(objectType), exact, nil } func normalizeDDLObjectType(value string) string { switch strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(value), " ", "_")) { case "TABLE": return "TABLE" case "VIEW": return "VIEW" case "MATERIALIZED_VIEW": return "MATERIALIZED_VIEW" default: return "" } } func (s *server) buildViewDDL(schema, name string) (string, error) { source, err := s.getViewSource(schema, name) if err != nil { return "", err } trimmed := strings.TrimSpace(source) upperSource := strings.ToUpper(trimmed) if strings.HasPrefix(upperSource, "CREATE ") || strings.HasPrefix(upperSource, "ALTER ") { return trimmed, nil } return fmt.Sprintf("CREATE OR REPLACE VIEW %s.%s AS\n%s", quoteIdentifier(schema), quoteIdentifier(name), trimmed), nil } func (s *server) getViewSource(schema, name string) (string, error) { db, err := s.requireDB() if err != nil { return "", err } viewName := strings.TrimSpace(name) var ddl string metadataErr := db.QueryRow( "SELECT DBMS_METADATA.GET_DDL('VIEW', :1, :2) FROM DUAL", viewName, schema, ).Scan(&ddl) if metadataErr == nil && strings.TrimSpace(ddl) != "" { return strings.TrimSpace(ddl), nil } var source string fallbackErr := db.QueryRow( "SELECT TEXT FROM ALL_VIEWS WHERE OWNER = :1 AND VIEW_NAME = :2", schema, viewName, ).Scan(&source) if fallbackErr == nil && strings.TrimSpace(source) != "" { return strings.TrimSpace(source), nil } if fallbackErr != nil && !errors.Is(fallbackErr, sql.ErrNoRows) { if metadataErr != nil { return "", fmt.Errorf( "failed to load view source for %s.%s: DBMS_METADATA: %v; ALL_VIEWS: %w", schema, viewName, metadataErr, fallbackErr, ) } return "", fmt.Errorf("failed to load view source for %s.%s from ALL_VIEWS: %w", schema, viewName, fallbackErr) } return "", fmt.Errorf("view source not found: %s.%s", schema, viewName) } func (s *server) buildTableDDL(schema, table string) (string, error) { columns, err := s.getColumns(schema, table) if err != nil { return "", err } if len(columns) == 0 { return "", fmt.Errorf("table not found: %s.%s", schema, table) } var builder strings.Builder builder.WriteString("CREATE TABLE ") builder.WriteString(quoteIdentifier(schema)) builder.WriteByte('.') builder.WriteString(quoteIdentifier(table)) builder.WriteString(" (\n") for i, column := range columns { if i > 0 { builder.WriteString(",\n") } builder.WriteString(" ") builder.WriteString(quoteIdentifier(column.Name)) builder.WriteByte(' ') builder.WriteString(oracleColumnTypeDDL(column)) if column.ColumnDefault != nil && strings.TrimSpace(*column.ColumnDefault) != "" { builder.WriteString(" DEFAULT ") builder.WriteString(strings.TrimSpace(*column.ColumnDefault)) } if !column.IsNullable { builder.WriteString(" NOT NULL") } } primary := make([]string, 0) for _, column := range columns { if column.IsPrimaryKey { primary = append(primary, quoteIdentifier(column.Name)) } } if len(primary) > 0 { builder.WriteString(",\n PRIMARY KEY (") builder.WriteString(strings.Join(primary, ", ")) builder.WriteByte(')') } builder.WriteString("\n)") return builder.String(), nil } func oracleColumnTypeDDL(column columnInfo) string { dataType := strings.ToUpper(strings.TrimSpace(column.DataType)) if dataType == "" { return "VARCHAR2(4000)" } if strings.Contains(dataType, "(") { return dataType } if isOracleCharacterType(dataType) && column.CharacterMaximumLength != nil && *column.CharacterMaximumLength > 0 { if unit := oracleCharacterLengthUnit(dataType, column.CharacterLengthUnit); unit != "" { return fmt.Sprintf("%s(%d %s)", dataType, *column.CharacterMaximumLength, unit) } return fmt.Sprintf("%s(%d)", dataType, *column.CharacterMaximumLength) } if dataType == "NUMBER" { if column.NumericPrecision != nil && *column.NumericPrecision > 0 { if column.NumericScale != nil && *column.NumericScale != 0 { return fmt.Sprintf("NUMBER(%d,%d)", *column.NumericPrecision, *column.NumericScale) } return fmt.Sprintf("NUMBER(%d)", *column.NumericPrecision) } return "NUMBER" } if (dataType == "FLOAT" || dataType == "BINARY_FLOAT" || dataType == "BINARY_DOUBLE") && column.NumericPrecision != nil && *column.NumericPrecision > 0 { return fmt.Sprintf("%s(%d)", dataType, *column.NumericPrecision) } return dataType } func oracleCharacterLengthUnit(dataType string, charUsed *string) string { switch dataType { case "CHAR", "VARCHAR", "VARCHAR2": default: return "" } if charUsed == nil { return "" } switch strings.ToUpper(strings.TrimSpace(*charUsed)) { case "B": return "BYTE" case "C": return "CHAR" default: return "" } } func isOracleCharacterType(dataType string) bool { switch dataType { case "CHAR", "VARCHAR2", "VARCHAR", "NCHAR", "NVARCHAR2", "RAW": return true default: return false } } func (s *server) getExplainInfo(sqlText, database, schema string, timeoutSecs int) (string, error) { if strings.TrimSpace(sqlText) == "" { return "", errors.New("sql is required") } db, err := s.requireDB() if err != nil { return "", err } ctx := context.Background() var cancel context.CancelFunc if timeoutSecs > 0 { ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSecs)*time.Second) } else { ctx, cancel = context.WithCancel(ctx) } defer cancel() conn, err := db.Conn(ctx) if err != nil { return "", err } defer conn.Close() targetSchema := oracleExplainTargetSchema(database, schema, s.params.Database) if targetSchema != "" { var originalSchema string if err := conn.QueryRowContext(ctx, "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL").Scan(&originalSchema); err != nil { return "", err } if !strings.EqualFold(originalSchema, targetSchema) { if _, err := conn.ExecContext(ctx, "ALTER SESSION SET CURRENT_SCHEMA = "+quoteIdentifier(targetSchema)); err != nil { return "", err } defer restoreOracleCurrentSchema(conn, originalSchema) } } statementID := "DBX_" + strings.ToUpper(strconv.FormatInt(time.Now().UnixNano(), 36)) defer cleanupOracleExplainPlan(conn, statementID) statementSQL := trimStatementSQL(sqlText) explainArgs := oracleExplainPlanBindArgs(statementSQL) if _, err := conn.ExecContext(ctx, "EXPLAIN PLAN SET STATEMENT_ID = '"+statementID+"' FOR "+statementSQL, explainArgs...); err != nil { return "", err } planRows, err := conn.QueryContext( ctx, "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY('PLAN_TABLE', :1, 'TYPICAL +PREDICATE'))", statementID, ) if err != nil { return "", err } defer planRows.Close() var builder strings.Builder for planRows.Next() { var line string if err := planRows.Scan(&line); err != nil { return "", err } builder.WriteString(line) builder.WriteByte('\n') } return strings.TrimSpace(builder.String()), planRows.Err() } func oracleExplainTargetSchema(database, schema, configuredDatabase string) string { if schema = strings.TrimSpace(schema); schema != "" { return schema } database = oracleConnectionDatabaseName(database) if database == "" || strings.EqualFold(database, oracleConnectionDatabaseName(configuredDatabase)) { return "" } return database } func cleanupOracleExplainPlan(conn *sql.Conn, statementID string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, _ = conn.ExecContext(ctx, "DELETE FROM PLAN_TABLE WHERE STATEMENT_ID = :1", statementID) } type oracleBindParam struct { Name string Positional bool } func oracleExplainPlanBindArgs(sqlText string) []any { params := oracleExplainPlanBindParams(sqlText) args := make([]any, 0, len(params)) for _, param := range params { if param.Positional { args = append(args, nil) continue } args = append(args, sql.Named(param.Name, nil)) } return args } func oracleExplainPlanBindParams(sqlText string) []oracleBindParam { params := make([]oracleBindParam, 0) seenNamed := map[string]bool{} for pos := 0; pos < len(sqlText); pos++ { switch sqlText[pos] { case '\'': pos = skipSingleQuotedSQL(sqlText, pos) case '"': pos = skipDoubleQuotedSQL(sqlText, pos) case 'q', 'Q': if end, ok := skipOracleAlternativeQuotedSQL(sqlText, pos); ok { pos = end } case '-': if pos+1 < len(sqlText) && sqlText[pos+1] == '-' { pos = skipLineCommentSQL(sqlText, pos) } case '/': if pos+1 < len(sqlText) && sqlText[pos+1] == '*' { pos = skipBlockCommentSQL(sqlText, pos) } case ':': param, end, ok := readOracleBindParam(sqlText, pos) if !ok { continue } if param.Positional { params = append(params, param) } else if key := strings.ToUpper(param.Name); !seenNamed[key] { seenNamed[key] = true params = append(params, param) } pos = end - 1 } } return params } func readOracleBindParam(sqlText string, pos int) (oracleBindParam, int, bool) { if pos < 0 || pos+1 >= len(sqlText) || sqlText[pos] != ':' { return oracleBindParam{}, pos, false } if pos > 0 && sqlText[pos-1] == ':' { return oracleBindParam{}, pos, false } next := sqlText[pos+1] if next >= '0' && next <= '9' { end := pos + 2 for end < len(sqlText) && sqlText[end] >= '0' && sqlText[end] <= '9' { end++ } return oracleBindParam{Name: sqlText[pos+1 : end], Positional: true}, end, true } if !isOracleIdentifierStart(next) { return oracleBindParam{}, pos, false } end := pos + 2 for end < len(sqlText) && isOracleIdentifierPart(sqlText[end]) { end++ } return oracleBindParam{Name: sqlText[pos+1 : end]}, end, true } func restoreOracleCurrentSchema(conn *sql.Conn, schema string) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, _ = conn.ExecContext(ctx, "ALTER SESSION SET CURRENT_SCHEMA = "+quoteIdentifier(schema)) } func (s *server) executeTransaction(params map[string]json.RawMessage) (queryResult, error) { if s.hasManualTransaction() { return queryResult{}, errors.New("cannot start a one-shot transaction while a manual transaction is open") } var payload struct { Statements []string `json:"statements"` Schema string `json:"schema"` } if err := decodeParams(params, &payload); err != nil { return queryResult{}, err } db, err := s.requireDB() if err != nil { return queryResult{}, err } tx, err := db.Begin() if err != nil { return queryResult{}, err } if strings.TrimSpace(payload.Schema) != "" { if _, err := tx.Exec("ALTER SESSION SET CURRENT_SCHEMA = " + quoteIdentifier(payload.Schema)); err != nil { tx.Rollback() return queryResult{}, err } } var affected int64 start := time.Now() for _, statement := range payload.Statements { statement = trimStatementSQL(statement) if statement == "" { continue } result, err := tx.Exec(statement) if err != nil { tx.Rollback() return queryResult{}, err } count, _ := result.RowsAffected() affected += count } if err := tx.Commit(); err != nil { return queryResult{}, err } return queryResult{ Columns: []string{}, Rows: [][]any{}, AffectedRows: affected, ExecutionTimeMS: time.Since(start).Milliseconds(), }, nil } func (s *server) executeQueryPage(opts queryOptions, pageSize int) (queryPageResult, error) { start := time.Now() if strings.TrimSpace(opts.Schema) != "" && !s.hasManualTransaction() { if err := s.setSchema(opts.Schema); err != nil { return queryPageResult{}, err } } sqlText := trimStatementSQL(opts.SQL) if !isQuerySQL(sqlText) { result, err := s.executeQuery(opts) return queryPageResult{ Columns: result.Columns, ColumnTypes: result.ColumnTypes, Rows: result.Rows, AffectedRows: result.AffectedRows, ExecutionTimeMS: result.ExecutionTimeMS, Truncated: result.Truncated, SessionID: nil, HasMore: false, }, err } rows, err := s.queryRowsWithOracleValueRewriteIfNeeded(sqlText, opts.TimeoutSecs, opts.DeferLOBs) if err != nil { return queryPageResult{}, err } columns, err := rows.Columns() if err != nil { s.closeRows(rows) return queryPageResult{}, err } columnTypes := columnTypeNames(rows) maxRows := opts.MaxRows if maxRows <= 0 { maxRows = defaultMaxRows } session := &querySession{rows: rows, columns: columns, columnTypes: columnTypes, remaining: maxRows} result, err := readQuerySessionPage(session, pageSize) result.ExecutionTimeMS = time.Since(start).Milliseconds() if err != nil { s.closeRows(rows) return queryPageResult{}, err } if result.HasMore { sessionID := s.storeQuerySession(session) result.SessionID = &sessionID } else { s.closeRows(rows) } return result, nil } func (s *server) fetchQueryPage(sessionID string, pageSize int) (queryPageResult, error) { session := s.sessions[sessionID] if session == nil { return queryPageResult{Columns: []string{}, ColumnTypes: []string{}, Rows: [][]any{}, SessionID: nil, HasMore: false}, nil } result, err := readQuerySessionPage(session, pageSize) if err != nil { s.closeQuerySession(sessionID) return queryPageResult{}, err } if result.HasMore { result.SessionID = &sessionID } else { s.closeQuerySession(sessionID) } return result, nil } func (s *server) storeQuerySession(session *querySession) string { s.nextSessionID++ sessionID := fmt.Sprintf("oracle-go-%d", s.nextSessionID) s.sessions[sessionID] = session return sessionID } func (s *server) startTableRead(opts queryOptions, pageSize int) (queryPageResult, error) { start := time.Now() if strings.TrimSpace(opts.Schema) != "" && !s.hasManualTransaction() { if err := s.setSchema(opts.Schema); err != nil { return queryPageResult{}, err } } sqlText := trimStatementSQL(opts.SQL) if !isQuerySQL(sqlText) { return queryPageResult{}, errors.New("table read requires a SELECT query") } rows, err := s.queryRowsWithOracleValueRewriteIfNeeded(sqlText, opts.TimeoutSecs, opts.DeferLOBs) if err != nil { return queryPageResult{}, err } columns, err := rows.Columns() if err != nil { s.closeRows(rows) return queryPageResult{}, err } columnTypes := columnTypeNames(rows) maxRows := opts.MaxRows if maxRows <= 0 { maxRows = defaultMaxRows } session := &querySession{rows: rows, columns: columns, columnTypes: columnTypes, remaining: maxRows} result, err := readQuerySessionPage(session, pageSize) result.ExecutionTimeMS = time.Since(start).Milliseconds() if err != nil { s.closeRows(rows) return queryPageResult{}, err } if result.HasMore { sessionID := s.storeTableReadSession(session) result.SessionID = &sessionID } else { s.closeRows(rows) } return result, nil } func (s *server) fetchTableReadPage(sessionID string, pageSize int) (queryPageResult, error) { session := s.tableReadSessions[sessionID] if session == nil { return queryPageResult{Columns: []string{}, ColumnTypes: []string{}, Rows: [][]any{}, SessionID: nil, HasMore: false}, nil } result, err := readQuerySessionPage(session, pageSize) if err != nil { s.closeTableReadSession(sessionID) return queryPageResult{}, err } if result.HasMore { result.SessionID = &sessionID } else { s.closeTableReadSession(sessionID) } return result, nil } func (s *server) storeTableReadSession(session *querySession) string { s.nextTableReadSessionID++ sessionID := fmt.Sprintf("oracle-go-table-%d", s.nextTableReadSessionID) s.tableReadSessions[sessionID] = session return sessionID } func (s *server) closeQuerySession(sessionID string) bool { session := s.sessions[sessionID] if session == nil { return false } s.closeRows(session.rows) delete(s.sessions, sessionID) return true } func (s *server) closeTableReadSession(sessionID string) bool { session := s.tableReadSessions[sessionID] if session == nil { return false } s.closeRows(session.rows) delete(s.tableReadSessions, sessionID) return true } func (s *server) closeAllQuerySessions() { for sessionID := range s.sessions { s.closeQuerySession(sessionID) } for sessionID := range s.tableReadSessions { s.closeTableReadSession(sessionID) } } func readQuerySessionPage(session *querySession, pageSize int) (queryPageResult, error) { if pageSize <= 0 { pageSize = defaultMaxRows } result := queryPageResult{Columns: session.columns, ColumnTypes: session.columnTypes, Rows: [][]any{}, SessionID: nil, HasMore: false} for len(result.Rows) < pageSize && session.remaining > 0 { if session.pending != nil { result.Rows = append(result.Rows, session.pending) session.pending = nil session.remaining-- continue } if !session.rows.Next() { return result, session.rows.Err() } row, err := scanRow(session.rows, len(session.columns), session.columnTypes) if err != nil { return queryPageResult{}, err } result.Rows = append(result.Rows, row) session.remaining-- } if session.remaining <= 0 { result.Truncated = true return result, nil } if session.rows.Next() { row, err := scanRow(session.rows, len(session.columns), session.columnTypes) if err != nil { return queryPageResult{}, err } session.pending = row result.HasMore = true return result, nil } return result, session.rows.Err() } func (s *server) executeQuery(opts queryOptions) (queryResult, error) { start := time.Now() if strings.TrimSpace(opts.Schema) != "" && !s.hasManualTransaction() { if err := s.setSchema(opts.Schema); err != nil { return queryResult{}, err } } sqlText := trimStatementSQL(opts.SQL) maxRows := opts.MaxRows if maxRows <= 0 { maxRows = defaultMaxRows } if isQuerySQL(sqlText) { result, err := s.executeSelect(sqlText, maxRows, opts.TimeoutSecs, opts.DeferLOBs) result.ExecutionTimeMS = time.Since(start).Milliseconds() return result, err } if _, err := s.requireDB(); err != nil { return queryResult{}, err } ctx, cancel := context.WithCancel(context.Background()) var timer *time.Timer if opts.TimeoutSecs > 0 { var t *time.Timer t = time.AfterFunc(time.Duration(opts.TimeoutSecs)*time.Second, func() { s.activeCancelMu.Lock() if s.activeTimer == t { cancel() } s.activeCancelMu.Unlock() }) timer = t } s.activeCancelMu.Lock() s.activeCancel = cancel s.activeTimer = timer s.activeCancelMu.Unlock() defer func() { cancel() s.activeCancelMu.Lock() s.activeCancel = nil if s.activeTimer != nil { s.activeTimer.Stop() s.activeTimer = nil } s.activeCancelMu.Unlock() }() execResult, err := s.execContext(ctx, sqlText) if err != nil { return queryResult{}, err } affected, _ := execResult.RowsAffected() return queryResult{Columns: []string{}, ColumnTypes: []string{}, Rows: [][]any{}, AffectedRows: affected, ExecutionTimeMS: time.Since(start).Milliseconds()}, nil } func (s *server) execContext(ctx context.Context, sqlText string) (sql.Result, error) { if s.manualTx != nil { return s.manualTx.ExecContext(ctx, sqlText) } db, err := s.requireDB() if err != nil { return nil, err } return db.ExecContext(ctx, sqlText) } func (s *server) executeSelect(sqlText string, maxRows int, timeoutSecs int, deferLOBs bool) (queryResult, error) { return executeOracleSelectWithXMLTypeRetry( sqlText, func(query string) (queryResult, error) { return s.executeSelectOnce(query, maxRows, timeoutSecs, deferLOBs) }, s.rewriteXMLTypeSelectSQL, ) } func executeOracleSelectWithXMLTypeRetry( sqlText string, execute func(string) (queryResult, error), rewrite func(string) (string, error), ) (queryResult, error) { result, err := execute(sqlText) if err == nil || !shouldRetryOracleXMLTypeRewrite(err) { return result, err } rewritten, rewriteErr := rewrite(sqlText) if rewriteErr != nil || rewritten == sqlText { return result, err } return execute(rewritten) } func shouldRetryOracleXMLTypeRewrite(err error) bool { if err == nil { return false } message := err.Error() return strings.Contains(message, "abnormal data representation for date") || strings.Contains(message, "TTC error: received code ") } func (s *server) executeSelectOnce(sqlText string, maxRows int, timeoutSecs int, deferLOBs bool) (queryResult, error) { rows, err := s.queryRowsWithOracleValueRewriteIfNeeded(sqlText, timeoutSecs, deferLOBs) if err != nil { return queryResult{}, err } defer s.closeRows(rows) columns, err := rows.Columns() if err != nil { return queryResult{}, err } columnTypes := columnTypeNames(rows) result := queryResult{Columns: columns, ColumnTypes: columnTypes, Rows: [][]any{}} for rows.Next() { if len(result.Rows) >= maxRows { result.Truncated = true break } values, err := scanRow(rows, len(columns), columnTypes) if err != nil { return queryResult{}, err } result.Rows = append(result.Rows, values) } return result, rows.Err() } func scanRow(rows *sql.Rows, columnCount int, columnTypes []string) ([]any, error) { values := make([]any, columnCount) scanTargets := make([]any, columnCount) for i := range values { scanTargets[i] = &values[i] } if err := rows.Scan(scanTargets...); err != nil { return nil, err } for i, value := range values { values[i] = normalizeValue(value, columnTypeAt(columnTypes, i)) } return values, nil } func columnTypeAt(columnTypes []string, index int) string { if index < 0 || index >= len(columnTypes) { return "" } return columnTypes[index] } func columnTypeNames(rows *sql.Rows) []string { types, err := rows.ColumnTypes() if err != nil { return []string{} } result := make([]string, 0, len(types)) for _, columnType := range types { result = append(result, columnType.DatabaseTypeName()) } return result } func (s *server) queryRowsWithOracleValueRewriteIfNeeded(sqlText string, timeoutSecs int, deferLOBs bool) (*sql.Rows, error) { if deferLOBs { rewritten, err := rewriteOracleSelectSQL(sqlText, s.loadOracleColumnMeta, true) if err == nil && rewritten != sqlText { return s.queryRowsWithTimeout(rewritten, nil, timeoutSecs) } } rows, err := s.queryRowsWithTimeout(sqlText, nil, timeoutSecs) if err != nil { return nil, err } typeNames := columnTypeNames(rows) if !oracleColumnTypeNamesContainXMLType(typeNames) { return rows, nil } rewritten, err := rewriteOracleSelectSQL(sqlText, s.loadOracleColumnMeta, false) if err != nil { s.closeRows(rows) return nil, err } if rewritten == sqlText { return rows, nil } // XMLTYPE keeps its metadata-triggered fallback so ordinary non-preview // queries do not run dictionary probes. s.closeRows(rows) return s.queryRowsWithTimeout(rewritten, nil, timeoutSecs) } func oracleColumnTypeNamesContainXMLType(typeNames []string) bool { for _, typeName := range typeNames { if isOracleXMLType(typeName) { return true } } return false } func (s *server) rewriteXMLTypeSelectSQL(sqlText string) (string, error) { return rewriteOracleXMLTypeSelectSQL(sqlText, s.loadOracleColumnMeta) } func rewriteOracleXMLTypeSelectSQL(sqlText string, loadColumns oracleColumnMetaLoader) (string, error) { return rewriteOracleSelectSQL(sqlText, loadColumns, false) } func rewriteOracleSelectSQL(sqlText string, loadColumns oracleColumnMetaLoader, deferLOBs bool) (string, error) { rewritten, _, err := rewriteOracleSelectSQLDepth(sqlText, loadColumns, deferLOBs, 0) return rewritten, err } func rewriteOracleSelectSQLDepth(sqlText string, loadColumns oracleColumnMetaLoader, deferLOBs bool, depth int) (string, bool, error) { if depth > 8 { return sqlText, false, nil } if rewritten, changed, handled, err := rewriteDirectOracleSelectSQL(sqlText, loadColumns, deferLOBs); handled || err != nil { return rewritten, changed, err } if deferLOBs { return rewriteOracleFullPassthroughInnerSelect(sqlText, loadColumns, depth) } rewritten, changed, err := rewriteNestedOracleSelects(sqlText, loadColumns, deferLOBs, depth) return rewritten, changed, err } func rewriteOracleFullPassthroughInnerSelect(sqlText string, loadColumns oracleColumnMetaLoader, depth int) (string, bool, error) { innerStart, innerEnd, ok := oracleFullPassthroughInnerSelectRange(sqlText) if !ok { return sqlText, false, nil } inner := sqlText[innerStart:innerEnd] rewritten, changed, err := rewriteOracleSelectSQLDepth(inner, loadColumns, true, depth+1) if err != nil || !changed { return sqlText, false, err } return sqlText[:innerStart] + rewritten + sqlText[innerEnd:], true, nil } func oracleFullPassthroughInnerSelectRange(sqlText string) (int, int, bool) { selectStart := leadingSQLSelectListStart(sqlText) if selectStart < 0 { return 0, 0, false } fromIdx := findTopLevelSQLKeyword(sqlText, selectStart, "from") if fromIdx < 0 { return 0, 0, false } prefix, selectList := splitOracleSelectListModifier(sqlText[selectStart:fromIdx]) if strings.TrimSpace(prefix) != "" { return 0, 0, false } items := splitTopLevelSQLList(selectList) if len(items) != 1 { return 0, 0, false } if _, ok := parseOracleStarSelectItem(items[0]); !ok { return 0, 0, false } open := skipSQLWhitespace(sqlText, fromIdx+len("from")) if open >= len(sqlText) || sqlText[open] != '(' { return 0, 0, false } close := findMatchingSQLParen(sqlText, open) if close < 0 || !startsWithSQLKeyword(trimLeadingSQLComments(sqlText[open+1:close]), "select") { return 0, 0, false } pos := skipSQLWhitespace(sqlText, close+1) if pos < len(sqlText) && sqlText[pos] != ';' && !nextKeywordIsOracleClause(sqlText[pos:]) { _, afterAlias, aliasOK := readOracleIdentifierToken(sqlText, pos) if !aliasOK { return 0, 0, false } pos = skipSQLWhitespace(sqlText, afterAlias) } if pos < len(sqlText) && sqlText[pos] != ';' && !nextKeywordIsOracleClause(sqlText[pos:]) { return 0, 0, false } return open + 1, close, true } func rewriteNestedOracleSelects(sqlText string, loadColumns oracleColumnMetaLoader, deferLOBs bool, depth int) (string, bool, error) { var builder strings.Builder changed := false last := 0 for pos := 0; pos < len(sqlText); pos++ { switch sqlText[pos] { case '\'': pos = skipSingleQuotedSQL(sqlText, pos) case '"': pos = skipDoubleQuotedSQL(sqlText, pos) case '-': if pos+1 < len(sqlText) && sqlText[pos+1] == '-' { pos = skipLineCommentSQL(sqlText, pos) } case '/': if pos+1 < len(sqlText) && sqlText[pos+1] == '*' { pos = skipBlockCommentSQL(sqlText, pos) } case '(': end := findMatchingSQLParen(sqlText, pos) if end < 0 { return sqlText, false, nil } inner := sqlText[pos+1 : end] if startsWithSQLKeyword(trimLeadingSQLComments(inner), "select") { rewrittenInner, innerChanged, err := rewriteOracleSelectSQLDepth(inner, loadColumns, deferLOBs, depth+1) if err != nil { return "", false, err } if innerChanged { builder.WriteString(sqlText[last : pos+1]) builder.WriteString(rewrittenInner) last = end changed = true } } pos = end } } if !changed { return sqlText, false, nil } builder.WriteString(sqlText[last:]) return builder.String(), true, nil } func rewriteDirectOracleSelectSQL(sqlText string, loadColumns oracleColumnMetaLoader, deferLOBs bool) (string, bool, bool, error) { selectStart := leadingSQLSelectListStart(sqlText) if selectStart < 0 { return sqlText, false, false, nil } fromIdx := findTopLevelSQLKeyword(sqlText, selectStart, "from") if fromIdx < 0 { return sqlText, false, false, nil } if deferLOBs && oracleSQLHasTopLevelSetOperator(sqlText, fromIdx+len("from")) { return sqlText, false, false, nil } selectListPrefix, selectList := splitOracleSelectListModifier(sqlText[selectStart:fromIdx]) deferSelectLOBs := deferLOBs && !startsWithSQLKeyword(strings.TrimSpace(selectListPrefix), "distinct") tableRef, ok := parseSingleOracleTableRef(sqlText[fromIdx+len("from"):]) if !ok { return sqlText, false, false, nil } items := splitTopLevelSQLList(selectList) if len(items) == 0 || !oracleSelectListMayReferenceTableColumns(items) { return sqlText, false, true, nil } columns, err := loadColumns(tableRef.Schema, tableRef.Table) if err != nil { return "", false, true, err } if deferSelectLOBs && oracleColumnsConflictWithLargeValueMarkers(columns) { deferSelectLOBs = false } if !oracleColumnsNeedValueRewrite(columns, deferSelectLOBs) { return sqlText, false, true, nil } rewrittenItems, changed := rewriteOracleSelectItems(items, columns, tableRef, deferSelectLOBs) if !changed { return sqlText, false, true, nil } var builder strings.Builder builder.WriteString(sqlText[:selectStart]) builder.WriteString(selectListPrefix) builder.WriteString(strings.Join(rewrittenItems, ", ")) builder.WriteByte(' ') builder.WriteString(sqlText[fromIdx:]) return builder.String(), true, true, nil } type oracleTableRef struct { Schema string Table string Alias string AliasText string } type oracleIdentifierToken struct { Name string Text string Quoted bool } func parseSingleOracleTableRef(fromSQL string) (oracleTableRef, bool) { pos := skipSQLWhitespace(fromSQL, 0) if pos >= len(fromSQL) || fromSQL[pos] == '(' { return oracleTableRef{}, false } first, next, ok := readOracleIdentifierToken(fromSQL, pos) if !ok { return oracleTableRef{}, false } ref := oracleTableRef{Table: first.Name} pos = skipSQLWhitespace(fromSQL, next) if pos < len(fromSQL) && fromSQL[pos] == '.' { second, afterSecond, ok := readOracleIdentifierToken(fromSQL, skipSQLWhitespace(fromSQL, pos+1)) if !ok { return oracleTableRef{}, false } ref.Schema = first.Name ref.Table = second.Name pos = skipSQLWhitespace(fromSQL, afterSecond) } if pos < len(fromSQL) { if strings.HasPrefix(strings.TrimLeft(fromSQL[pos:], " \t\r\n"), ",") { return oracleTableRef{}, false } if nextKeywordIsOracleClause(fromSQL[pos:]) { return ref, true } if startsWithSQLKeyword(fromSQL[pos:], "join") || startsWithSQLKeyword(fromSQL[pos:], "inner") || startsWithSQLKeyword(fromSQL[pos:], "left") || startsWithSQLKeyword(fromSQL[pos:], "right") || startsWithSQLKeyword(fromSQL[pos:], "full") || startsWithSQLKeyword(fromSQL[pos:], "cross") { return oracleTableRef{}, false } alias, afterAlias, ok := readOracleIdentifierToken(fromSQL, pos) if ok && !oracleIdentifierIsClause(alias.Name) { ref.Alias = alias.Name ref.AliasText = alias.Text pos = skipSQLWhitespace(fromSQL, afterAlias) } if strings.HasPrefix(strings.TrimLeft(fromSQL[pos:], " \t\r\n"), ",") || startsWithSQLKeyword(fromSQL[pos:], "join") || startsWithSQLKeyword(fromSQL[pos:], "inner") || startsWithSQLKeyword(fromSQL[pos:], "left") || startsWithSQLKeyword(fromSQL[pos:], "right") || startsWithSQLKeyword(fromSQL[pos:], "full") || startsWithSQLKeyword(fromSQL[pos:], "cross") { return oracleTableRef{}, false } } return ref, true } func splitOracleSelectListModifier(selectList string) (string, string) { trimmedLeft := strings.TrimLeft(selectList, " \t\r\n") prefixLen := len(selectList) - len(trimmedLeft) for _, keyword := range []string{"distinct", "all"} { if startsWithSQLKeyword(trimmedLeft, keyword) { modifierEnd := prefixLen + len(keyword) for modifierEnd < len(selectList) && isSQLWhitespace(selectList[modifierEnd]) { modifierEnd++ } return selectList[:modifierEnd], selectList[modifierEnd:] } } return selectList[:prefixLen], selectList[prefixLen:] } func oracleSQLHasTopLevelSetOperator(sqlText string, start int) bool { for _, keyword := range []string{"union", "minus", "intersect"} { if findTopLevelSQLKeyword(sqlText, start, keyword) >= 0 { return true } } return false } func oracleSelectListMayReferenceTableColumns(items []string) bool { for _, item := range items { if _, ok := parseOracleStarSelectItem(item); ok { return true } if _, _, _, ok := parseOracleColumnSelectItem(item); ok { return true } } return false } func rewriteOracleSelectItems(items []string, columns []oracleColumnMeta, tableRef oracleTableRef, deferLOBs bool) ([]string, bool) { columnsByName := map[string]oracleColumnMeta{} for _, column := range columns { columnsByName[oracleIdentifierKey(column.Name)] = column } rewritten := make([]string, 0, len(items)) changed := false sourceIndex := 0 for _, item := range items { if qualifier, ok := parseOracleStarSelectItem(item); ok && oracleQualifierMatchesTable(qualifier, tableRef) { for _, column := range columns { columnRef := oracleColumnRef(tableRef.AliasText, column.Name) outputAlias := quoteIdentifier(column.Name) if isOracleXMLType(column.DataType) && !deferLOBs { rewritten = append(rewritten, oracleXMLSerializeExpression(columnRef, outputAlias)) } else if deferLOBs { if expressions, ok := oracleDeferredLOBExpressions(columnRef, outputAlias, sourceIndex, column.DataType); ok { rewritten = append(rewritten, expressions...) } else { rewritten = append(rewritten, columnRef) } } else { rewritten = append(rewritten, columnRef) } sourceIndex++ } changed = true continue } qualifier, column, alias, ok := parseOracleColumnSelectItem(item) if ok && oracleQualifierMatchesTable(qualifier, tableRef) { if meta, exists := columnsByName[oracleIdentifierKey(column.Name)]; exists { outputAlias := alias if outputAlias == "" { outputAlias = quoteIdentifier(meta.Name) } columnRef := oracleColumnRef(qualifier, meta.Name) if isOracleXMLType(meta.DataType) && !deferLOBs { rewritten = append(rewritten, oracleXMLSerializeExpression(columnRef, outputAlias)) changed = true sourceIndex++ continue } if deferLOBs { if expressions, isLOB := oracleDeferredLOBExpressions(columnRef, outputAlias, sourceIndex, meta.DataType); isLOB { rewritten = append(rewritten, expressions...) changed = true sourceIndex++ continue } } } } rewritten = append(rewritten, item) sourceIndex++ } return rewritten, changed } func oracleDeferredLOBExpressions(columnRef, outputAlias string, sourceIndex int, dataType string) ([]string, bool) { kind, placeholder, ok := oracleDeferredLOBKind(dataType) if !ok { return nil, false } valueExpression := fmt.Sprintf("CASE WHEN %s IS NULL THEN NULL ELSE '%s' END AS %s", columnRef, placeholder, outputAlias) markerAlias := fmt.Sprintf("%s%s_%d", largeValueBytesColumnPrefix, kind, sourceIndex) markerExpression := fmt.Sprintf("CASE WHEN %s IS NULL THEN NULL ELSE 'D:1' END AS %s", columnRef, quoteIdentifier(markerAlias)) return []string{valueExpression, markerExpression}, true } func oracleDeferredLOBKind(dataType string) (kind, placeholder string, ok bool) { switch strings.ToUpper(strings.TrimSpace(dataType)) { case "CLOB": return "C", "", true case "NCLOB": return "N", "", true case "BLOB": return "L", "", true case "BFILE": return "F", "", true case "XMLTYPE", "SYS.XMLTYPE": return "C", "", true default: return "", "", false } } func oracleXMLSerializeExpression(columnRef, alias string) string { // go-ora v2.9.0 does not fully decode Oracle XMLTYPE result payloads, // especially when 11g switches larger values to locator-based transfer. return fmt.Sprintf("XMLSERIALIZE(CONTENT %s AS CLOB) AS %s", columnRef, alias) } func oracleColumnRef(qualifier, column string) string { if strings.TrimSpace(qualifier) == "" { return quoteIdentifier(column) } return qualifier + "." + quoteIdentifier(column) } func parseOracleStarSelectItem(item string) (string, bool) { trimmed := strings.TrimSpace(item) if trimmed == "*" { return "", true } qualifier, pos, ok := readOracleIdentifierToken(trimmed, 0) if !ok { return "", false } pos = skipSQLWhitespace(trimmed, pos) if pos >= len(trimmed) || trimmed[pos] != '.' { return "", false } pos = skipSQLWhitespace(trimmed, pos+1) if pos < len(trimmed) && trimmed[pos] == '*' && strings.TrimSpace(trimmed[pos+1:]) == "" { return qualifier.Text, true } return "", false } func parseOracleColumnSelectItem(item string) (qualifier string, column oracleIdentifierToken, alias string, ok bool) { trimmed := strings.TrimSpace(item) first, pos, ok := readOracleIdentifierToken(trimmed, 0) if !ok { return "", oracleIdentifierToken{}, "", false } column = first pos = skipSQLWhitespace(trimmed, pos) if pos < len(trimmed) && trimmed[pos] == '.' { second, afterSecond, ok := readOracleIdentifierToken(trimmed, skipSQLWhitespace(trimmed, pos+1)) if !ok { return "", oracleIdentifierToken{}, "", false } qualifier = first.Text column = second pos = skipSQLWhitespace(trimmed, afterSecond) } if pos >= len(trimmed) { return qualifier, column, "", true } if startsWithSQLKeyword(trimmed[pos:], "as") { aliasToken, afterAlias, ok := readOracleIdentifierToken(trimmed, skipSQLWhitespace(trimmed, pos+len("as"))) if !ok || strings.TrimSpace(trimmed[afterAlias:]) != "" { return "", oracleIdentifierToken{}, "", false } return qualifier, column, aliasToken.Text, true } aliasToken, afterAlias, ok := readOracleIdentifierToken(trimmed, pos) if !ok || strings.TrimSpace(trimmed[afterAlias:]) != "" { return "", oracleIdentifierToken{}, "", false } return qualifier, column, aliasToken.Text, true } func oracleQualifierMatchesTable(qualifier string, tableRef oracleTableRef) bool { if strings.TrimSpace(qualifier) == "" { return true } key := oracleIdentifierKey(unquoteOracleIdentifierText(qualifier)) if tableRef.Alias != "" && key == oracleIdentifierKey(tableRef.Alias) { return true } return key == oracleIdentifierKey(tableRef.Table) } func oracleColumnsNeedValueRewrite(columns []oracleColumnMeta, deferLOBs bool) bool { for _, column := range columns { if isOracleXMLType(column.DataType) || (deferLOBs && isOracleDeferredLOBType(column.DataType)) { return true } } return false } func oracleColumnsConflictWithLargeValueMarkers(columns []oracleColumnMeta) bool { for _, column := range columns { if strings.HasPrefix(strings.ToUpper(column.Name), largeValueBytesColumnPrefix) { return true } } return false } func isOracleDeferredLOBType(dataType string) bool { _, _, ok := oracleDeferredLOBKind(dataType) return ok } func isOracleXMLType(dataType string) bool { normalized := strings.ToUpper(strings.TrimSpace(dataType)) return normalized == "XMLTYPE" || normalized == "SYS.XMLTYPE" } func leadingSQLSelectListStart(sqlText string) int { trimmed := trimLeadingSQLComments(sqlText) prefixLen := len(sqlText) - len(trimmed) if !startsWithSQLKeyword(trimmed, "select") { return -1 } return prefixLen + len("select") } func splitTopLevelSQLList(value string) []string { var result []string start := 0 depth := 0 for pos := 0; pos < len(value); pos++ { switch value[pos] { case '\'': pos = skipSingleQuotedSQL(value, pos) case '"': pos = skipDoubleQuotedSQL(value, pos) case '-': if pos+1 < len(value) && value[pos+1] == '-' { pos = skipLineCommentSQL(value, pos) } case '/': if pos+1 < len(value) && value[pos+1] == '*' { pos = skipBlockCommentSQL(value, pos) } case '(': depth++ case ')': if depth > 0 { depth-- } case ',': if depth == 0 { result = append(result, strings.TrimSpace(value[start:pos])) start = pos + 1 } } } tail := strings.TrimSpace(value[start:]) if tail != "" { result = append(result, tail) } return result } func findTopLevelSQLKeyword(sqlText string, start int, keyword string) int { depth := 0 for pos := start; pos < len(sqlText); pos++ { switch sqlText[pos] { case '\'': pos = skipSingleQuotedSQL(sqlText, pos) case '"': pos = skipDoubleQuotedSQL(sqlText, pos) case '-': if pos+1 < len(sqlText) && sqlText[pos+1] == '-' { pos = skipLineCommentSQL(sqlText, pos) } case '/': if pos+1 < len(sqlText) && sqlText[pos+1] == '*' { pos = skipBlockCommentSQL(sqlText, pos) } case '(': depth++ case ')': if depth > 0 { depth-- } default: if depth == 0 && sqlKeywordAt(sqlText, pos, keyword) { return pos } } } return -1 } func findMatchingSQLParen(sqlText string, open int) int { depth := 0 for pos := open; pos < len(sqlText); pos++ { switch sqlText[pos] { case '\'': pos = skipSingleQuotedSQL(sqlText, pos) case '"': pos = skipDoubleQuotedSQL(sqlText, pos) case '-': if pos+1 < len(sqlText) && sqlText[pos+1] == '-' { pos = skipLineCommentSQL(sqlText, pos) } case '/': if pos+1 < len(sqlText) && sqlText[pos+1] == '*' { pos = skipBlockCommentSQL(sqlText, pos) } case '(': depth++ case ')': depth-- if depth == 0 { return pos } } } return -1 } func readOracleIdentifierToken(value string, pos int) (oracleIdentifierToken, int, bool) { pos = skipSQLWhitespace(value, pos) if pos >= len(value) { return oracleIdentifierToken{}, pos, false } if value[pos] == '"' { end := pos + 1 var builder strings.Builder for end < len(value) { if value[end] == '"' { if end+1 < len(value) && value[end+1] == '"' { builder.WriteByte('"') end += 2 continue } return oracleIdentifierToken{Name: builder.String(), Text: value[pos : end+1], Quoted: true}, end + 1, true } builder.WriteByte(value[end]) end++ } return oracleIdentifierToken{}, pos, false } if !isOracleIdentifierStart(value[pos]) { return oracleIdentifierToken{}, pos, false } end := pos + 1 for end < len(value) && isOracleIdentifierPart(value[end]) { end++ } text := value[pos:end] return oracleIdentifierToken{Name: strings.ToUpper(text), Text: text}, end, true } func unquoteOracleIdentifierText(value string) string { value = strings.TrimSpace(value) if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { return strings.ReplaceAll(value[1:len(value)-1], `""`, `"`) } return strings.ToUpper(value) } func oracleIdentifierKey(value string) string { return strings.ToUpper(strings.TrimSpace(value)) } func oracleIdentifierIsClause(value string) bool { switch oracleIdentifierKey(value) { case "WHERE", "GROUP", "ORDER", "HAVING", "CONNECT", "START", "MODEL", "FETCH", "OFFSET", "FOR", "AS", "UNION", "MINUS", "INTERSECT": return true default: return false } } func nextKeywordIsOracleClause(value string) bool { trimmed := strings.TrimSpace(value) if trimmed == "" { return true } token, _, ok := readOracleIdentifierToken(trimmed, 0) return ok && oracleIdentifierIsClause(token.Name) } func isOracleIdentifierStart(ch byte) bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '$' || ch == '#' } func isOracleIdentifierPart(ch byte) bool { return isOracleIdentifierStart(ch) || (ch >= '0' && ch <= '9') } func skipSQLWhitespace(value string, pos int) int { for pos < len(value) && isSQLWhitespace(value[pos]) { pos++ } return pos } func isSQLWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' } func skipSingleQuotedSQL(value string, pos int) int { pos++ for pos < len(value) { if value[pos] == '\'' { if pos+1 < len(value) && value[pos+1] == '\'' { pos += 2 continue } return pos } pos++ } return len(value) - 1 } func skipOracleAlternativeQuotedSQL(value string, pos int) (int, bool) { if pos+2 >= len(value) || (value[pos] != 'q' && value[pos] != 'Q') || value[pos+1] != '\'' { return pos, false } open := value[pos+2] close := open switch open { case '[': close = ']' case '{': close = '}' case '(': close = ')' case '<': close = '>' } for end := pos + 3; end+1 < len(value); end++ { if value[end] == close && value[end+1] == '\'' { return end + 1, true } } return len(value) - 1, true } func skipDoubleQuotedSQL(value string, pos int) int { pos++ for pos < len(value) { if value[pos] == '"' { if pos+1 < len(value) && value[pos+1] == '"' { pos += 2 continue } return pos } pos++ } return len(value) - 1 } func skipLineCommentSQL(value string, pos int) int { for pos < len(value) { if value[pos] == '\n' || value[pos] == '\r' { return pos } pos++ } return len(value) - 1 } func skipBlockCommentSQL(value string, pos int) int { end := strings.Index(value[pos+2:], "*/") if end < 0 { return len(value) - 1 } return pos + end + 3 } func (s *server) setSchema(schema string) error { sqlText := "ALTER SESSION SET CURRENT_SCHEMA = " + quoteIdentifier(schema) if s.manualTx != nil { _, err := s.manualTx.Exec(sqlText) return err } if s.manualConn != nil { _, err := s.manualConn.ExecContext(context.Background(), sqlText) return err } db, err := s.requireDB() if err != nil { return err } _, err = db.Exec(sqlText) return err } func (s *server) queryRows(sqlText string, args []any) (*sql.Rows, error) { return s.queryRowsWithTimeout(sqlText, args, 0) } func (s *server) queryRowsWithTimeout(sqlText string, args []any, timeoutSecs int) (*sql.Rows, error) { if _, err := s.requireDB(); err != nil { return nil, err } ctx, cancel := context.WithCancel(context.Background()) var timer *time.Timer if timeoutSecs > 0 { var t *time.Timer t = time.AfterFunc(time.Duration(timeoutSecs)*time.Second, func() { s.activeCancelMu.Lock() if s.activeTimer == t { s.activeTimedOut = true cancel() } s.activeCancelMu.Unlock() }) timer = t } s.activeCancelMu.Lock() s.activeCancel = cancel s.activeTimer = timer s.activeTimedOut = false s.activeCancelMu.Unlock() var rows *sql.Rows var queryErr error if s.manualTx != nil { rows, queryErr = s.manualTx.QueryContext(ctx, sqlText, args...) } else { db, err := s.requireDB() if err != nil { cancel() return nil, err } rows, queryErr = db.QueryContext(ctx, sqlText, args...) } s.activeCancelMu.Lock() s.activeCancel = nil if s.activeTimer != nil { s.activeTimer.Stop() s.activeTimer = nil } timedOut := s.activeTimedOut if queryErr != nil { cancel() } else if timedOut { cancel() if rows != nil { rows.Close() } queryErr = fmt.Errorf("query timed out after %ds", timeoutSecs) } else { s.activeRows[rows] = cancel } s.activeCancelMu.Unlock() return rows, queryErr } func (s *server) cancelActiveQuery() { s.activeCancelMu.Lock() cancels := make([]context.CancelFunc, 0, len(s.activeRows)+1) if s.activeCancel != nil { cancels = append(cancels, s.activeCancel) } for _, cancel := range s.activeRows { cancels = append(cancels, cancel) } s.activeCancelMu.Unlock() for _, cancel := range cancels { cancel() } } func (s *server) closeRows(rows *sql.Rows) error { if rows == nil { return nil } s.activeCancelMu.Lock() cancel := s.activeRows[rows] delete(s.activeRows, rows) s.activeCancelMu.Unlock() if cancel != nil { cancel() } return rows.Close() } func decodeParams(params map[string]json.RawMessage, target any) error { if params == nil { params = map[string]json.RawMessage{} } data, err := json.Marshal(params) if err != nil { return err } return json.Unmarshal(data, target) } func stringParam(params map[string]json.RawMessage, key string) string { if params == nil || len(params[key]) == 0 { return "" } var value string _ = json.Unmarshal(params[key], &value) return value } func stringSliceParam(params map[string]json.RawMessage, key string) []string { if params == nil || len(params[key]) == 0 { return nil } var value []string if err := json.Unmarshal(params[key], &value); err != nil { return nil } return value } func intParam(params map[string]json.RawMessage, key string) int { if params == nil || len(params[key]) == 0 { return 0 } var value int _ = json.Unmarshal(params[key], &value) return value } func boolParam(params map[string]json.RawMessage, key string) bool { if params == nil || len(params[key]) == 0 { return false } var value bool _ = json.Unmarshal(params[key], &value) return value } func errorResponse(id json.RawMessage, err error) response { return response{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: -1, Message: err.Error()}} } func parseURLParams(raw string) map[string]string { result := map[string]string{} values, err := url.ParseQuery(raw) if err != nil { return result } for key, items := range values { if len(items) > 0 { result[key] = items[len(items)-1] } } return result } func trimStatementSQL(sqlText string) string { trimmed := stripTrailingSlashDelimiter(strings.TrimSpace(sqlText)) if isOraclePlSQLBlock(trimmed) { return trimmed } return strings.TrimRight(trimmed, "; \t\r\n") } func stripTrailingSlashDelimiter(sqlText string) string { trimmed := strings.TrimSpace(sqlText) if !strings.HasSuffix(trimmed, "/") { return trimmed } slashStart := len(trimmed) - 1 lineStart := strings.LastIndex(trimmed[:slashStart], "\n") + 1 if strings.TrimSpace(trimmed[lineStart:slashStart]) != "" { return trimmed } beforeSlash := strings.TrimSpace(trimmed[:lineStart]) // SQL*Plus uses a standalone slash to execute PL/SQL blocks; go-ora needs // only the block text and not that client-side delimiter. if isOraclePlSQLBlock(beforeSlash) { return beforeSlash } return trimmed } func isOraclePlSQLBlock(sqlText string) bool { trimmed := strings.TrimSpace(sqlText) start := trimLeadingSQLComments(trimmed) if !oraclePlSQLBlockStartRegexp.MatchString(start) { return false } upper := strings.ToUpper(trimmed) if oraclePlSQLBlockEndRegexp.MatchString(upper) { return true } matches := oracleNamedPlSQLBlockEndRegexp.FindStringSubmatch(upper) if len(matches) != 2 { return false } switch matches[1] { case "IF", "LOOP", "CASE": return false default: return true } } func isQuerySQL(sqlText string) bool { executable := trimLeadingSQLComments(sqlText) return startsWithSQLKeyword(executable, "select") || startsWithSQLKeyword(executable, "with") } func trimLeadingSQLComments(sqlText string) string { remaining := strings.TrimSpace(sqlText) for { switch { case strings.HasPrefix(remaining, "--"): lineEnd := strings.IndexAny(remaining, "\r\n") if lineEnd < 0 { return "" } remaining = strings.TrimSpace(remaining[lineEnd+1:]) case strings.HasPrefix(remaining, "/*"): commentEnd := strings.Index(remaining[2:], "*/") if commentEnd < 0 { return "" } remaining = strings.TrimSpace(remaining[commentEnd+4:]) default: return remaining } } } func startsWithSQLKeyword(sqlText, keyword string) bool { sqlText = strings.TrimSpace(sqlText) if len(sqlText) < len(keyword) || !strings.EqualFold(sqlText[:len(keyword)], keyword) { return false } if len(sqlText) == len(keyword) { return true } next := sqlText[len(keyword)] return !((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z') || (next >= '0' && next <= '9') || next == '_' || next == '$') } func sqlKeywordAt(sqlText string, pos int, keyword string) bool { if pos < 0 || pos+len(keyword) > len(sqlText) || !strings.EqualFold(sqlText[pos:pos+len(keyword)], keyword) { return false } if pos > 0 && isOracleIdentifierPart(sqlText[pos-1]) { return false } if pos+len(keyword) >= len(sqlText) { return true } return !isOracleIdentifierPart(sqlText[pos+len(keyword)]) } func quoteIdentifier(value string) string { return `"` + strings.ReplaceAll(value, `"`, `""`) + `"` } func normalizeValue(value any, columnTypeName string) any { switch v := value.(type) { case nil: return nil case []byte: // Oracle RAW-like columns are binary data; decoding them as text produces mojibake. if isOracleBinaryColumnType(columnTypeName) { return bytesToHex(v) } return string(v) case time.Time: // Oracle DATE and plain TIMESTAMP are wall-clock values; adding an offset makes clients shift them. if isOracleTimezoneLessDateTime(columnTypeName) { return v.Format("2006-01-02T15:04:05.999999999") } return v.Format(time.RFC3339Nano) case int64, float64, bool, string: return v case fmt.Stringer: return v.String() default: return fmt.Sprint(v) } } func isOracleTimezoneLessDateTime(columnTypeName string) bool { normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(columnTypeName), " ", "")) if normalized == "DATE" || normalized == "TIMESTAMPDTY" || normalized == "TIMESTAMP" { return true } return strings.HasPrefix(normalized, "TIMESTAMP(") && strings.HasSuffix(normalized, ")") } func isOracleBinaryColumnType(columnTypeName string) bool { normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(columnTypeName), " ", "")) switch normalized { case "RAW", "VARRAW", "LONGRAW", "LONGVARRAW", "BLOB", "BFILE", "OCIBLOBLOCATOR", "OCIFILELOCATOR": return true default: return false } } func bytesToHex(bytes []byte) string { const digits = "0123456789abcdef" result := make([]byte, 2+len(bytes)*2) result[0] = '0' result[1] = 'x' for i, b := range bytes { result[2+i*2] = digits[b>>4] result[3+i*2] = digits[b&0x0f] } return string(result) } func emptyIfNil[T any](values []T) []T { if values == nil { return []T{} } return values } func intPtrFromString(value string) *int { if value == "" { return nil } parsed, err := strconv.Atoi(value) if err != nil { return nil } return &parsed }