mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 08:18:52 +00:00
5404 lines
159 KiB
Go
5404 lines
159 KiB
Go
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
|
|
|
|
// A normal data-grid page contains 100 rows. Prefetching a little more than
|
|
// one page reduces round trips while reading subsequent pages without
|
|
// buffering the much larger export limit.
|
|
const oracleDefaultPrefetchRows = "256"
|
|
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"`
|
|
}
|
|
|
|
// oracleDriverPanicError keeps a third-party driver panic on the request path
|
|
// so the agent can try a safe projection rewrite instead of terminating the
|
|
// process and breaking the RPC stream.
|
|
type oracleDriverPanicError struct {
|
|
value any
|
|
}
|
|
|
|
func (e oracleDriverPanicError) Error() string {
|
|
return fmt.Sprintf("oracle driver panic: %v", e.value)
|
|
}
|
|
|
|
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
|
|
DataTypeOwner 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) (resp response, shutdown bool) {
|
|
var req request
|
|
// Last-resort guard: a panic anywhere on the request path must not kill
|
|
// the agent process and break the RPC stream, so convert it to a readable
|
|
// error response instead of an end-of-stream failure.
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
resp = errorResponse(req.ID, fmt.Errorf("agent request panic: %v", recovered))
|
|
shutdown = false
|
|
}
|
|
}()
|
|
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, shouldShutdown, 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}, shouldShutdown
|
|
}
|
|
|
|
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, DATA_TYPE_OWNER
|
|
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
|
|
var dataTypeOwner sql.NullString
|
|
if err := rows.Scan(&item.Name, &item.DataType, &dataTypeOwner); err != nil {
|
|
return nil, err
|
|
}
|
|
if dataTypeOwner.Valid {
|
|
item.DataTypeOwner = dataTypeOwner.String
|
|
}
|
|
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 == "MATERIALIZED_VIEW" || 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
|
|
}
|
|
result, session, err := s.runPagedOracleSelect(sqlText, opts, pageSize, start)
|
|
if err != nil {
|
|
return queryPageResult{}, err
|
|
}
|
|
if session != nil {
|
|
sessionID := s.storeQuerySession(session)
|
|
result.SessionID = &sessionID
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// runPagedOracleSelect executes a SELECT through the value-rewrite path and
|
|
// reads its first page. It returns the live session when more pages remain.
|
|
// When the first page panics while decoding an unsupported column type (before
|
|
// any row was streamed to the client), it retries once with the placeholder
|
|
// projection so the remaining columns stay readable; panics on later pages
|
|
// surface as errors instead.
|
|
func (s *server) runPagedOracleSelect(sqlText string, opts queryOptions, pageSize int, start time.Time) (queryPageResult, *querySession, error) {
|
|
for attempt := 0; ; attempt++ {
|
|
rows, err := s.queryRowsWithOracleValueRewriteIfNeeded(sqlText, opts.TimeoutSecs, opts.DeferLOBs)
|
|
if err != nil {
|
|
return queryPageResult{}, nil, err
|
|
}
|
|
columns, err := rows.Columns()
|
|
if err != nil {
|
|
s.closeRows(rows)
|
|
return queryPageResult{}, nil, 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)
|
|
var panicErr oracleDriverPanicError
|
|
if attempt == 0 && errors.As(err, &panicErr) {
|
|
if placeholder, ok := oraclePlaceholderRetrySQL(sqlText, s.loadOracleColumnMeta); ok {
|
|
sqlText = placeholder
|
|
continue
|
|
}
|
|
}
|
|
return queryPageResult{}, nil, err
|
|
}
|
|
if result.HasMore {
|
|
return result, session, nil
|
|
}
|
|
s.closeRows(rows)
|
|
return result, nil, nil
|
|
}
|
|
}
|
|
|
|
// oraclePlaceholderRetrySQL returns the placeholder projection used to retry a
|
|
// query whose first page panicked while decoding values of an unsupported
|
|
// column type. It reports false when no placeholder rewrite applies.
|
|
func oraclePlaceholderRetrySQL(sqlText string, loadColumns oracleColumnMetaLoader) (string, bool) {
|
|
placeholder, err := rewriteOracleSelectSQL(sqlText, loadColumns, true)
|
|
if err != nil || placeholder == sqlText {
|
|
return "", false
|
|
}
|
|
return placeholder, true
|
|
}
|
|
|
|
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")
|
|
}
|
|
result, session, err := s.runPagedOracleSelect(sqlText, opts, pageSize, start)
|
|
if err != nil {
|
|
return queryPageResult{}, err
|
|
}
|
|
if session != nil {
|
|
sessionID := s.storeTableReadSession(session)
|
|
result.SessionID = &sessionID
|
|
}
|
|
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) (result queryPageResult, err error) {
|
|
// go-ora can panic while decoding row values for column types it does not
|
|
// support (for example custom object types). Convert those panics to errors
|
|
// so row iteration failures never terminate the agent process and break
|
|
// the RPC stream.
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
result = queryPageResult{}
|
|
err = oracleDriverPanicError{value: recovered}
|
|
}
|
|
}()
|
|
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
|
|
}
|
|
// A full page is enough evidence that another page may exist. Do not read
|
|
// one extra row just to decide has_more: with a prefetch boundary this
|
|
// forces another Oracle round trip before the first page can be displayed.
|
|
if len(result.Rows) >= pageSize {
|
|
result.HasMore = 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 {
|
|
var panicErr oracleDriverPanicError
|
|
if errors.As(err, &panicErr) {
|
|
rewritten, rewriteErr := rewriteOracleSelectSQL(sqlText, s.loadOracleColumnMeta, false)
|
|
if rewriteErr == nil && rewritten != sqlText {
|
|
rewrittenRows, rewrittenErr := s.queryRowsWithTimeout(rewritten, nil, timeoutSecs)
|
|
if rewrittenErr == nil {
|
|
return rewrittenRows, nil
|
|
}
|
|
// When the server's ArcSDE extproc agent is broken, the
|
|
// SDE.ST_AsText rewrite itself fails with ORA-28595 and would
|
|
// hide the remaining columns; retry once with the placeholder
|
|
// projection so the query stays readable.
|
|
if placeholder, ok := oracleExtprocFallbackSQL(sqlText, s.loadOracleColumnMeta, rewrittenErr); ok {
|
|
if placeholderRows, placeholderErr := s.queryRowsWithTimeout(placeholder, nil, timeoutSecs); placeholderErr == nil {
|
|
return placeholderRows, nil
|
|
}
|
|
}
|
|
return nil, rewrittenErr
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// isOracleExtprocAgentFailure reports whether err is an Oracle extproc agent
|
|
// failure (ORA-28595, typically raised through SDE.ST_GEOMETRY_SHAPELIB_PKG or
|
|
// SDE.ST_GEOMETRY_OPERATORS) that makes every SDE.ST_GEOMETRY function call
|
|
// unusable on the server. Matching stays deliberately narrow: the ORA code
|
|
// must be present; SDE package names alone never trigger it.
|
|
func isOracleExtprocAgentFailure(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
// Accept both the "ORA-28595" form and a bare "28595" code.
|
|
return strings.Contains(strings.ToUpper(err.Error()), "28595")
|
|
}
|
|
|
|
// oracleExtprocFallbackSQL returns the placeholder projection to retry with
|
|
// when the panic-fallback SDE.ST_AsText rewrite failed because of a broken
|
|
// extproc agent (ORA-28595). It reports false when the error is unrelated or
|
|
// no placeholder rewrite applies.
|
|
func oracleExtprocFallbackSQL(sqlText string, loadColumns oracleColumnMetaLoader, rewriteErr error) (string, bool) {
|
|
if !isOracleExtprocAgentFailure(rewriteErr) {
|
|
return "", false
|
|
}
|
|
placeholder, err := rewriteOracleSelectSQL(sqlText, loadColumns, true)
|
|
if err != nil || placeholder == sqlText {
|
|
return "", false
|
|
}
|
|
return placeholder, true
|
|
}
|
|
|
|
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 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 isOracleGeometryType(column) && !deferLOBs {
|
|
rewritten = append(rewritten, oracleGeometryExpression(column, columnRef, outputAlias))
|
|
} else if isOracleXMLType(column.DataType) && !deferLOBs {
|
|
rewritten = append(rewritten, oracleXMLSerializeExpression(columnRef, outputAlias))
|
|
} else if deferLOBs {
|
|
if expressions, ok := oracleDeferredLOBExpressions(columnRef, outputAlias, sourceIndex, column); 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 isOracleGeometryType(meta) && !deferLOBs {
|
|
rewritten = append(rewritten, oracleGeometryExpression(meta, columnRef, outputAlias))
|
|
changed = true
|
|
sourceIndex++
|
|
continue
|
|
}
|
|
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); isLOB {
|
|
rewritten = append(rewritten, expressions...)
|
|
changed = true
|
|
sourceIndex++
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rewritten = append(rewritten, item)
|
|
sourceIndex++
|
|
}
|
|
return rewritten, changed
|
|
}
|
|
|
|
func oracleDeferredLOBExpressions(columnRef, outputAlias string, sourceIndex int, column oracleColumnMeta) ([]string, bool) {
|
|
kind, placeholder, ok := oracleDeferredLOBKind(column.DataType)
|
|
valueRef := columnRef
|
|
if isOracleGeometryType(column) {
|
|
kind, placeholder, ok = "C", oracleGeometryPlaceholder(column), true
|
|
}
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
valueExpression := fmt.Sprintf("CASE WHEN %s IS NULL THEN NULL ELSE '%s' END AS %s", valueRef, 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", valueRef, quoteIdentifier(markerAlias))
|
|
return []string{valueExpression, markerExpression}, true
|
|
}
|
|
|
|
func oracleSTGeometryExpression(columnRef, alias string) string {
|
|
return fmt.Sprintf("SDE.ST_AsText(%s) AS %s", columnRef, alias)
|
|
}
|
|
|
|
func oracleSDOGeometryExpression(columnRef, alias string) string {
|
|
return fmt.Sprintf("SDO_UTIL.TO_WKTGEOMETRY(%s) AS %s", columnRef, alias)
|
|
}
|
|
|
|
func oracleGeometryExpression(column oracleColumnMeta, columnRef, alias string) string {
|
|
if isOracleSDOGeometry(column) {
|
|
return oracleSDOGeometryExpression(columnRef, alias)
|
|
}
|
|
return oracleSTGeometryExpression(columnRef, alias)
|
|
}
|
|
|
|
func oracleGeometryPlaceholder(column oracleColumnMeta) string {
|
|
if isOracleSDOGeometry(column) {
|
|
return "<SDO_GEOMETRY>"
|
|
}
|
|
return "<ST_GEOMETRY>"
|
|
}
|
|
|
|
func oracleDeferredLOBKind(dataType string) (kind, placeholder string, ok bool) {
|
|
switch strings.ToUpper(strings.TrimSpace(dataType)) {
|
|
case "CLOB":
|
|
return "C", "<CLOB>", true
|
|
case "NCLOB":
|
|
return "N", "<NCLOB>", true
|
|
case "BLOB":
|
|
return "L", "<BLOB>", true
|
|
case "BFILE":
|
|
return "F", "<BFILE>", true
|
|
case "XMLTYPE", "SYS.XMLTYPE":
|
|
return "C", "<XMLTYPE>", 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 isOracleGeometryType(column) || 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 isOracleSTGeometry(column oracleColumnMeta) bool {
|
|
// Esri's SDE.ST_GEOMETRY is an unregistered Oracle object for go-ora;
|
|
// SDE.ST_AsText provides the supported CLOB representation for reads.
|
|
dataType := strings.ToUpper(strings.TrimSpace(column.DataType))
|
|
if dataType == "SDE.ST_GEOMETRY" {
|
|
return true
|
|
}
|
|
return dataType == "ST_GEOMETRY" && strings.EqualFold(strings.TrimSpace(column.DataTypeOwner), "SDE")
|
|
}
|
|
|
|
func isOracleSDOGeometry(column oracleColumnMeta) bool {
|
|
// Oracle Spatial's MDSYS.SDO_GEOMETRY is likewise an unregistered object
|
|
// type for go-ora and panics on scan; SDO_UTIL.TO_WKTGEOMETRY provides
|
|
// the supported CLOB (WKT) representation for reads. Most schemas only
|
|
// see it through the PUBLIC synonym, so DATA_TYPE_OWNER is commonly
|
|
// "PUBLIC" rather than "MDSYS" and isn't checked here.
|
|
return strings.ToUpper(strings.TrimSpace(column.DataType)) == "SDO_GEOMETRY"
|
|
}
|
|
|
|
func isOracleGeometryType(column oracleColumnMeta) bool {
|
|
return isOracleSTGeometry(column) || isOracleSDOGeometry(column)
|
|
}
|
|
|
|
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) (rows *sql.Rows, queryErr 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()
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
cancel()
|
|
if timer != nil {
|
|
timer.Stop()
|
|
}
|
|
if rows != nil {
|
|
_ = rows.Close()
|
|
}
|
|
s.activeCancelMu.Lock()
|
|
s.activeCancel = nil
|
|
s.activeTimer = nil
|
|
s.activeTimedOut = false
|
|
if rows != nil {
|
|
delete(s.activeRows, rows)
|
|
}
|
|
s.activeCancelMu.Unlock()
|
|
rows = nil
|
|
queryErr = oracleDriverPanicError{value: recovered}
|
|
}
|
|
}()
|
|
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
|
|
}
|