feat(oracle): expand package members in schema tree
This commit is contained in:
12 files changed
+508
-75
No files matched your search
@@ -232,6 +232,7 @@ type completionAssistantCandidate struct {
|
|||||||
ParentName *string `json:"parent_name"`
|
ParentName *string `json:"parent_name"`
|
||||||
Comment *string `json:"comment"`
|
Comment *string `json:"comment"`
|
||||||
DataType *string `json:"data_type"`
|
DataType *string `json:"data_type"`
|
||||||
|
Signature *string `json:"signature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type completionAssistantResponse struct {
|
type completionAssistantResponse struct {
|
||||||
@@ -1845,6 +1846,10 @@ func (s *server) oracleCompletionValidSynonymTargets(targets []oracleCompletionS
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) completionAssistantRoutines(request completionAssistantRequest, preferredSchema string, limit int) (completionAssistantResponse, error) {
|
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)
|
query := oracleCompletionRoutinesQuery(request, preferredSchema, limit+1)
|
||||||
rows, err := s.queryRows(query.SQL, query.Args)
|
rows, err := s.queryRows(query.SQL, query.Args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1886,6 +1891,176 @@ func (s *server) completionAssistantRoutines(request completionAssistantRequest,
|
|||||||
return completionAssistantResponse{Candidates: candidates, Incomplete: incomplete}, nil
|
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 {
|
func stringPointer(value string) *string {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -1983,31 +2158,11 @@ func oracleCompletionSynonymTargetsQuery(targets []oracleCompletionSynonymTarget
|
|||||||
|
|
||||||
func oracleCompletionRoutinesQuery(request completionAssistantRequest, preferredSchema string, limit int) oracleMetadataListQuery {
|
func oracleCompletionRoutinesQuery(request completionAssistantRequest, preferredSchema string, limit int) oracleMetadataListQuery {
|
||||||
pattern := oracleCompletionLikePattern(request.Mask, request.MatchMode)
|
pattern := oracleCompletionLikePattern(request.Mask, request.MatchMode)
|
||||||
args := make([]any, 0, 6)
|
args := make([]any, 0, 5)
|
||||||
baseSQL := `
|
baseSQL := `
|
||||||
SELECT o.OWNER, o.OBJECT_NAME, o.OBJECT_TYPE, CAST(NULL AS VARCHAR2(128)) AS PARENT_NAME
|
SELECT o.OWNER, o.OBJECT_NAME, o.OBJECT_TYPE, CAST(NULL AS VARCHAR2(128)) AS PARENT_NAME
|
||||||
FROM ALL_OBJECTS o
|
FROM ALL_OBJECTS o
|
||||||
WHERE o.OBJECT_TYPE IN ('FUNCTION', 'PROCEDURE', 'PACKAGE')`
|
WHERE o.OBJECT_TYPE IN ('FUNCTION', 'PROCEDURE', 'PACKAGE')`
|
||||||
if parentName := strings.ToUpper(strings.TrimSpace(request.ParentName)); parentName != "" {
|
|
||||||
args = append(args, parentName)
|
|
||||||
parentParam := len(args)
|
|
||||||
baseSQL = fmt.Sprintf(`
|
|
||||||
SELECT p.OWNER,
|
|
||||||
p.PROCEDURE_NAME AS OBJECT_NAME,
|
|
||||||
CASE WHEN EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM ALL_ARGUMENTS a
|
|
||||||
WHERE a.OWNER = p.OWNER
|
|
||||||
AND a.OBJECT_ID = p.OBJECT_ID
|
|
||||||
AND a.SUBPROGRAM_ID = p.SUBPROGRAM_ID
|
|
||||||
AND a.POSITION = 0
|
|
||||||
) THEN 'FUNCTION' ELSE 'PROCEDURE' END AS OBJECT_TYPE,
|
|
||||||
p.OBJECT_NAME AS PARENT_NAME
|
|
||||||
FROM ALL_PROCEDURES p
|
|
||||||
WHERE p.OBJECT_TYPE = 'PACKAGE'
|
|
||||||
AND p.PROCEDURE_NAME IS NOT NULL
|
|
||||||
AND p.OBJECT_NAME = :%d`, parentParam)
|
|
||||||
}
|
|
||||||
args = append(args, pattern)
|
args = append(args, pattern)
|
||||||
nameParam := len(args)
|
nameParam := len(args)
|
||||||
|
|
||||||
@@ -2038,6 +2193,52 @@ WHERE p.OBJECT_TYPE = 'PACKAGE'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func oracleCompletionNamePredicate(column string, parameter int, caseSensitive bool) string {
|
||||||
if caseSensitive {
|
if caseSensitive {
|
||||||
return fmt.Sprintf("%s LIKE :%d ESCAPE '\\'", column, parameter)
|
return fmt.Sprintf("%s LIKE :%d ESCAPE '\\'", column, parameter)
|
||||||
|
|||||||
@@ -1706,13 +1706,13 @@ func TestOracleCompletionTablesQueryScopesExplicitSchema(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOracleCompletionRoutinesQueryUsesPublicPackageMetadata(t *testing.T) {
|
func TestOracleCompletionRoutinesQueryUsesPublicPackageMetadata(t *testing.T) {
|
||||||
query := oracleCompletionRoutinesQuery(completionAssistantRequest{
|
query := oracleCompletionPackageRoutinesQuery(completionAssistantRequest{
|
||||||
Schema: "HR",
|
Schema: "HR",
|
||||||
ParentSchema: "HR",
|
ParentSchema: "HR",
|
||||||
ParentName: "PAYROLL",
|
ParentName: "PAYROLL",
|
||||||
ObjectKinds: []string{"routine"},
|
ObjectKinds: []string{"routine"},
|
||||||
Mask: "CALC",
|
Mask: "CALC",
|
||||||
}, "HR", 200)
|
}, "HR")
|
||||||
sqlText := strings.ToUpper(query.SQL)
|
sqlText := strings.ToUpper(query.SQL)
|
||||||
|
|
||||||
if !strings.Contains(sqlText, "ALL_PROCEDURES") || !strings.Contains(sqlText, "ALL_ARGUMENTS") {
|
if !strings.Contains(sqlText, "ALL_PROCEDURES") || !strings.Contains(sqlText, "ALL_ARGUMENTS") {
|
||||||
@@ -1721,14 +1721,84 @@ func TestOracleCompletionRoutinesQueryUsesPublicPackageMetadata(t *testing.T) {
|
|||||||
if strings.Contains(sqlText, "ALL_SOURCE") || strings.Contains(sqlText, "PACKAGE BODY") {
|
if strings.Contains(sqlText, "ALL_SOURCE") || strings.Contains(sqlText, "PACKAGE BODY") {
|
||||||
t.Fatalf("package completion must not expose private package body source: %s", query.SQL)
|
t.Fatalf("package completion must not expose private package body source: %s", query.SQL)
|
||||||
}
|
}
|
||||||
if !strings.Contains(sqlText, "P.OBJECT_NAME = :1") || !strings.Contains(sqlText, "UPPER(OBJECT_NAME) LIKE UPPER(:2)") || !strings.Contains(sqlText, "AND OWNER = :3") {
|
if !strings.Contains(sqlText, "P.OWNER = :1") || !strings.Contains(sqlText, "P.OBJECT_NAME = :2") || !strings.Contains(sqlText, "UPPER(P.PROCEDURE_NAME) LIKE UPPER(:3)") {
|
||||||
t.Fatalf("package completion should scope package and owner: %s", query.SQL)
|
t.Fatalf("package completion should scope package and owner: %s", query.SQL)
|
||||||
}
|
}
|
||||||
if len(query.Args) != 6 || query.Args[0] != "PAYROLL" || query.Args[1] != "CALC%" || query.Args[2] != "HR" {
|
if !strings.Contains(sqlText, "A.OBJECT_ID = P.OBJECT_ID") || !strings.Contains(sqlText, "A.SUBPROGRAM_ID = P.SUBPROGRAM_ID") || !strings.Contains(sqlText, "A.DATA_LEVEL = 0") {
|
||||||
|
t.Fatalf("package completion should join complete top-level argument metadata: %s", query.SQL)
|
||||||
|
}
|
||||||
|
if strings.Contains(sqlText, "ROWNUM") {
|
||||||
|
t.Fatalf("package completion must not truncate argument rows before overload grouping: %s", query.SQL)
|
||||||
|
}
|
||||||
|
wantArgs := []any{"HR", "PAYROLL", "CALC%"}
|
||||||
|
if !reflect.DeepEqual(query.Args, wantArgs) {
|
||||||
t.Fatalf("unexpected package completion args: %#v", query.Args)
|
t.Fatalf("unexpected package completion args: %#v", query.Args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOracleCompletionPackageCandidatesPreserveOverloadsAndReturnTypes(t *testing.T) {
|
||||||
|
rows := []oraclePackageRoutineRow{
|
||||||
|
{
|
||||||
|
owner: "HR", parentName: "PAYROLL", name: "CALCULATE", objectID: 42, subprogramID: 1,
|
||||||
|
position: sql.NullInt64{Int64: 1, Valid: true}, sequence: sql.NullInt64{Int64: 1, Valid: true},
|
||||||
|
argumentName: sql.NullString{String: "P_VALUE", Valid: true}, inOut: sql.NullString{String: "IN", Valid: true}, dataType: sql.NullString{String: "NUMBER", Valid: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner: "HR", parentName: "PAYROLL", name: "CALCULATE", objectID: 42, subprogramID: 2,
|
||||||
|
position: sql.NullInt64{Int64: 1, Valid: true}, sequence: sql.NullInt64{Int64: 1, Valid: true},
|
||||||
|
argumentName: sql.NullString{String: "P_VALUE", Valid: true}, inOut: sql.NullString{String: "IN", Valid: true}, dataType: sql.NullString{String: "VARCHAR2", Valid: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner: "HR", parentName: "PAYROLL", name: "ITEM_COUNT", objectID: 42, subprogramID: 3,
|
||||||
|
position: sql.NullInt64{Int64: 0, Valid: true}, sequence: sql.NullInt64{Int64: 1, Valid: true}, dataType: sql.NullString{String: "NUMBER", Valid: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner: "HR", parentName: "PAYROLL", name: "ITEM_COUNT", objectID: 42, subprogramID: 3,
|
||||||
|
position: sql.NullInt64{Int64: 1, Valid: true}, sequence: sql.NullInt64{Int64: 2, Valid: true},
|
||||||
|
argumentName: sql.NullString{String: "P_ACTIVE", Valid: true}, inOut: sql.NullString{String: "IN/OUT", Valid: true}, dataType: sql.NullString{String: "NUMBER", Valid: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response := oracleCompletionPackageCandidates(rows, "XE", 10)
|
||||||
|
if response.Incomplete {
|
||||||
|
t.Fatal("complete package metadata should not be marked incomplete")
|
||||||
|
}
|
||||||
|
if len(response.Candidates) != 3 {
|
||||||
|
t.Fatalf("overloads should remain distinct candidates: %#v", response.Candidates)
|
||||||
|
}
|
||||||
|
want := []struct {
|
||||||
|
name string
|
||||||
|
kind string
|
||||||
|
signature string
|
||||||
|
returnType string
|
||||||
|
}{
|
||||||
|
{name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN NUMBER"},
|
||||||
|
{name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN VARCHAR2"},
|
||||||
|
{name: "ITEM_COUNT", kind: "function", signature: "P_ACTIVE IN OUT NUMBER", returnType: "NUMBER"},
|
||||||
|
}
|
||||||
|
for index, expected := range want {
|
||||||
|
candidate := response.Candidates[index]
|
||||||
|
if candidate.Name != expected.name || candidate.Kind != expected.kind || candidate.Signature == nil || *candidate.Signature != expected.signature {
|
||||||
|
t.Fatalf("candidate %d = %#v, want name=%q kind=%q signature=%q", index, candidate, expected.name, expected.kind, expected.signature)
|
||||||
|
}
|
||||||
|
if expected.returnType == "" {
|
||||||
|
if candidate.DataType != nil {
|
||||||
|
t.Fatalf("candidate %d return type = %#v, want nil", index, candidate.DataType)
|
||||||
|
}
|
||||||
|
} else if candidate.DataType == nil || *candidate.DataType != expected.returnType {
|
||||||
|
t.Fatalf("candidate %d return type = %#v, want %q", index, candidate.DataType, expected.returnType)
|
||||||
|
}
|
||||||
|
if candidate.ParentSchema == nil || *candidate.ParentSchema != "HR" || candidate.ParentName == nil || *candidate.ParentName != "PAYROLL" {
|
||||||
|
t.Fatalf("candidate %d lost package identity: %#v", index, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
limited := oracleCompletionPackageCandidates(rows, "XE", 2)
|
||||||
|
if !limited.Incomplete || len(limited.Candidates) != 2 {
|
||||||
|
t.Fatalf("member limit should apply after overload grouping: %#v", limited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOracleCompletionLikePatternSupportsPrefixAndContains(t *testing.T) {
|
func TestOracleCompletionLikePatternSupportsPrefixAndContains(t *testing.T) {
|
||||||
if got := oracleCompletionLikePattern(`A_%`, "prefix"); got != `A\_\%%` {
|
if got := oracleCompletionLikePattern(`A_%`, "prefix"); got != `A\_\%%` {
|
||||||
t.Fatalf("prefix pattern = %q", got)
|
t.Fatalf("prefix pattern = %q", got)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
const xuguPackageSpecSQL = `
|
const xuguPackageSpecSQL = `
|
||||||
@@ -201,8 +202,8 @@ func stringPointerOrNil(value string) *string {
|
|||||||
if value == "" {
|
if value == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
copy := value
|
result := value
|
||||||
return ©
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseXuguPackageMembers(spec string) []xuguPackageMember {
|
func parseXuguPackageMembers(spec string) []xuguPackageMember {
|
||||||
@@ -212,20 +213,21 @@ func parseXuguPackageMembers(spec string) []xuguPackageMember {
|
|||||||
index = next
|
index = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch spec[index] {
|
current, width := utf8.DecodeRuneInString(spec[index:])
|
||||||
|
switch current {
|
||||||
case '(':
|
case '(':
|
||||||
parenDepth++
|
parenDepth++
|
||||||
index++
|
index += width
|
||||||
continue
|
continue
|
||||||
case ')':
|
case ')':
|
||||||
if parenDepth > 0 {
|
if parenDepth > 0 {
|
||||||
parenDepth--
|
parenDepth--
|
||||||
}
|
}
|
||||||
index++
|
index += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if parenDepth != 0 || !isXuguIdentifierStart(rune(spec[index])) {
|
if parenDepth != 0 || !isXuguIdentifierStart(current) {
|
||||||
index++
|
index += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wordStart := index
|
wordStart := index
|
||||||
@@ -277,8 +279,9 @@ func xuguFunctionReturnType(fragment string) string {
|
|||||||
index = next
|
index = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !isXuguIdentifierStart(rune(fragment[index])) {
|
current, width := utf8.DecodeRuneInString(fragment[index:])
|
||||||
index++
|
if !isXuguIdentifierStart(current) {
|
||||||
|
index += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
start := index
|
start := index
|
||||||
@@ -298,7 +301,8 @@ func findXuguDeclarationEnd(value string, start int) int {
|
|||||||
index = next
|
index = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch value[index] {
|
current, width := utf8.DecodeRuneInString(value[index:])
|
||||||
|
switch current {
|
||||||
case '(':
|
case '(':
|
||||||
depth++
|
depth++
|
||||||
case ')':
|
case ')':
|
||||||
@@ -310,7 +314,7 @@ func findXuguDeclarationEnd(value string, start int) int {
|
|||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index++
|
index += width
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
@@ -322,7 +326,8 @@ func findXuguClosingParen(value string, open int) (int, bool) {
|
|||||||
index = next
|
index = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch value[index] {
|
current, width := utf8.DecodeRuneInString(value[index:])
|
||||||
|
switch current {
|
||||||
case '(':
|
case '(':
|
||||||
depth++
|
depth++
|
||||||
case ')':
|
case ')':
|
||||||
@@ -331,7 +336,7 @@ func findXuguClosingParen(value string, open int) (int, bool) {
|
|||||||
return index, true
|
return index, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index++
|
index += width
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@@ -342,21 +347,24 @@ func readXuguSQLIdentifier(value string, index int) (string, int, bool) {
|
|||||||
}
|
}
|
||||||
if value[index] == '"' {
|
if value[index] == '"' {
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
for cursor := index + 1; cursor < len(value); cursor++ {
|
for cursor := index + 1; cursor < len(value); {
|
||||||
if value[cursor] != '"' {
|
current, width := utf8.DecodeRuneInString(value[cursor:])
|
||||||
builder.WriteByte(value[cursor])
|
if current != '"' {
|
||||||
|
builder.WriteRune(current)
|
||||||
|
cursor += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cursor+1 < len(value) && value[cursor+1] == '"' {
|
if cursor+1 < len(value) && value[cursor+1] == '"' {
|
||||||
builder.WriteByte('"')
|
builder.WriteByte('"')
|
||||||
cursor++
|
cursor += 2
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return builder.String(), cursor + 1, true
|
return builder.String(), cursor + 1, true
|
||||||
}
|
}
|
||||||
return "", index, false
|
return "", index, false
|
||||||
}
|
}
|
||||||
if !isXuguIdentifierStart(rune(value[index])) {
|
current, _ := utf8.DecodeRuneInString(value[index:])
|
||||||
|
if !isXuguIdentifierStart(current) {
|
||||||
return "", index, false
|
return "", index, false
|
||||||
}
|
}
|
||||||
next := scanXuguIdentifier(value, index)
|
next := scanXuguIdentifier(value, index)
|
||||||
@@ -364,8 +372,12 @@ func readXuguSQLIdentifier(value string, index int) (string, int, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func scanXuguIdentifier(value string, index int) int {
|
func scanXuguIdentifier(value string, index int) int {
|
||||||
for index < len(value) && isXuguIdentifierPart(rune(value[index])) {
|
for index < len(value) {
|
||||||
index++
|
current, width := utf8.DecodeRuneInString(value[index:])
|
||||||
|
if !isXuguIdentifierPart(current) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
index += width
|
||||||
}
|
}
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
@@ -380,8 +392,9 @@ func isXuguIdentifierPart(value rune) bool {
|
|||||||
|
|
||||||
func skipXuguSQLSpaceAndComments(value string, index int) int {
|
func skipXuguSQLSpaceAndComments(value string, index int) int {
|
||||||
for index < len(value) {
|
for index < len(value) {
|
||||||
if unicode.IsSpace(rune(value[index])) {
|
current, width := utf8.DecodeRuneInString(value[index:])
|
||||||
index++
|
if unicode.IsSpace(current) {
|
||||||
|
index += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if next, ok := skipXuguSQLComment(value, index); ok {
|
if next, ok := skipXuguSQLComment(value, index); ok {
|
||||||
@@ -401,12 +414,14 @@ func skipXuguSQLLiteralOrComment(value string, index int) (int, bool) {
|
|||||||
return index, false
|
return index, false
|
||||||
}
|
}
|
||||||
quote := value[index]
|
quote := value[index]
|
||||||
for cursor := index + 1; cursor < len(value); cursor++ {
|
for cursor := index + 1; cursor < len(value); {
|
||||||
if value[cursor] != quote {
|
current, width := utf8.DecodeRuneInString(value[cursor:])
|
||||||
|
if current != rune(quote) {
|
||||||
|
cursor += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cursor+1 < len(value) && value[cursor+1] == quote {
|
if cursor+1 < len(value) && value[cursor+1] == quote {
|
||||||
cursor++
|
cursor += 2
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return cursor + 1, true
|
return cursor + 1, true
|
||||||
@@ -442,9 +457,10 @@ func compactXuguSQLFragment(value string) string {
|
|||||||
index = next
|
index = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if unicode.IsSpace(rune(value[index])) {
|
current, width := utf8.DecodeRuneInString(value[index:])
|
||||||
|
if unicode.IsSpace(current) {
|
||||||
spacePending = builder.Len() > 0
|
spacePending = builder.Len() > 0
|
||||||
index++
|
index += width
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if spacePending && builder.Len() > 0 {
|
if spacePending && builder.Len() > 0 {
|
||||||
@@ -456,8 +472,8 @@ func compactXuguSQLFragment(value string) string {
|
|||||||
index = next
|
index = next
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
builder.WriteByte(value[index])
|
builder.WriteRune(current)
|
||||||
index++
|
index += width
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(builder.String())
|
return strings.TrimSpace(builder.String())
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,21 @@ END;`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseXuguPackageMembersHandlesUnicodeIdentifiers(t *testing.T) {
|
||||||
|
spec := `CREATE PACKAGE 中文包 AS
|
||||||
|
PROCEDURE 处理订单(订单号 IN VARCHAR, 数量 IN NUMERIC);
|
||||||
|
FUNCTION 统计数量(分类 IN VARCHAR) RETURN NUMERIC;
|
||||||
|
END;`
|
||||||
|
got := parseXuguPackageMembers(spec)
|
||||||
|
want := []xuguPackageMember{
|
||||||
|
{Name: "处理订单", Kind: "PROCEDURE", Signature: "订单号 IN VARCHAR, 数量 IN NUMERIC"},
|
||||||
|
{Name: "统计数量", Kind: "FUNCTION", Signature: "分类 IN VARCHAR", ReturnType: "NUMERIC"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("unexpected Unicode package members:\n got: %#v\nwant: %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseXuguPackageMembersIgnoresBodyOnlyDeclarations(t *testing.T) {
|
func TestParseXuguPackageMembersIgnoresBodyOnlyDeclarations(t *testing.T) {
|
||||||
spec := `CREATE PACKAGE pkg AS
|
spec := `CREATE PACKAGE pkg AS
|
||||||
PROCEDURE public_proc(p_value INT);
|
PROCEDURE public_proc(p_value INT);
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ import {
|
|||||||
supportsTableTruncate,
|
supportsTableTruncate,
|
||||||
supportsTableStructureEditing,
|
supportsTableStructureEditing,
|
||||||
supportsTransfer,
|
supportsTransfer,
|
||||||
|
supportsPackageMemberExpansion,
|
||||||
usesTreeSchemaMode,
|
usesTreeSchemaMode,
|
||||||
isSingleDatabase,
|
isSingleDatabase,
|
||||||
} from "@/lib/database/databaseCapabilities";
|
} from "@/lib/database/databaseCapabilities";
|
||||||
@@ -622,8 +623,8 @@ async function toggle() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === "package" && node.connectionId && connectionStore.getConfig(node.connectionId)?.db_type === "xugu") {
|
if (node.type === "package" && node.connectionId && supportsPackageMemberExpansion(currentDatabaseType())) {
|
||||||
await connectionStore.loadXuguPackageMembers(node);
|
await connectionStore.loadPackageMembers(node);
|
||||||
emitNodeToggled(node, wasExpanded);
|
emitNodeToggled(node, wasExpanded);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
@@ -13,6 +13,9 @@ const connectionStore = {
|
|||||||
canUseLoadedTreeNodeToggle: vi.fn(() => true),
|
canUseLoadedTreeNodeToggle: vi.fn(() => true),
|
||||||
releaseCollapsedTreeNodeChildren: vi.fn(),
|
releaseCollapsedTreeNodeChildren: vi.fn(),
|
||||||
getConfig: vi.fn(() => ({ db_type: "mysql" })),
|
getConfig: vi.fn(() => ({ db_type: "mysql" })),
|
||||||
|
loadPackageMembers: vi.fn(async (node: TreeNode) => {
|
||||||
|
node.isExpanded = true;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("@/stores/connectionStore", () => ({
|
vi.mock("@/stores/connectionStore", () => ({
|
||||||
@@ -41,6 +44,7 @@ afterEach(() => {
|
|||||||
connectionStore.treeNodes = [];
|
connectionStore.treeNodes = [];
|
||||||
connectionStore.sidebarSearchQuery = "";
|
connectionStore.sidebarSearchQuery = "";
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
connectionStore.getConfig.mockReturnValue({ db_type: "mysql" });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("SidebarTreeRuntimeHost expansion", () => {
|
describe("SidebarTreeRuntimeHost expansion", () => {
|
||||||
@@ -78,4 +82,38 @@ describe("SidebarTreeRuntimeHost expansion", () => {
|
|||||||
expect(toggled).toHaveBeenCalledWith(renderedGroup, false);
|
expect(toggled).toHaveBeenCalledWith(renderedGroup, false);
|
||||||
expect(liveGroup.isExpanded).toBe(false);
|
expect(liveGroup.isExpanded).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads Oracle package members through the shared expansion path", async () => {
|
||||||
|
const packageNode: TreeNode = {
|
||||||
|
id: "oracle:XE:APP:package:BUSINESS_API",
|
||||||
|
label: "BUSINESS_API",
|
||||||
|
type: "package",
|
||||||
|
connectionId: "oracle",
|
||||||
|
database: "XE",
|
||||||
|
schema: "APP",
|
||||||
|
isExpanded: false,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
connectionStore.treeNodes = [packageNode];
|
||||||
|
connectionStore.getConfig.mockReturnValue({ db_type: "oracle" });
|
||||||
|
|
||||||
|
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||||
|
const toggled = vi.fn();
|
||||||
|
const app = createApp(
|
||||||
|
defineComponent({
|
||||||
|
setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: packageNode, depth: 0, onNodeToggled: toggled }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mountedApps.push(app);
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.append(container);
|
||||||
|
app.use(i18n);
|
||||||
|
app.mount(container);
|
||||||
|
|
||||||
|
host.value?.toggleNode(packageNode);
|
||||||
|
await vi.waitFor(() => expect(connectionStore.loadPackageMembers).toHaveBeenCalledWith(packageNode));
|
||||||
|
|
||||||
|
expect(packageNode.isExpanded).toBe(true);
|
||||||
|
expect(toggled).toHaveBeenCalledWith(packageNode, true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { normalizeSidebarObjectKind, sidebarObjectKindsForDatabase } from "@/lib/database/databaseObjectCapabilities";
|
import { normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, supportsPackageMemberExpansion } from "@/lib/database/databaseObjectCapabilities";
|
||||||
|
|
||||||
describe("databaseObjectCapabilities", () => {
|
describe("databaseObjectCapabilities", () => {
|
||||||
it("exposes supported programmable objects for Dameng", () => {
|
it("exposes supported programmable objects for Dameng", () => {
|
||||||
@@ -11,6 +11,13 @@ describe("databaseObjectCapabilities", () => {
|
|||||||
expect(sidebarObjectKindsForDatabase("postgres")).not.toContain("SYNONYM");
|
expect(sidebarObjectKindsForDatabase("postgres")).not.toContain("SYNONYM");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("expands package members only for implemented database paths", () => {
|
||||||
|
expect(supportsPackageMemberExpansion("oracle")).toBe(true);
|
||||||
|
expect(supportsPackageMemberExpansion("xugu")).toBe(true);
|
||||||
|
expect(supportsPackageMemberExpansion("dameng")).toBe(false);
|
||||||
|
expect(supportsPackageMemberExpansion("opengauss")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("exposes only tables for HBase namespaces", () => {
|
it("exposes only tables for HBase namespaces", () => {
|
||||||
expect(sidebarObjectKindsForDatabase("hbase")).toEqual(["TABLE"]);
|
expect(sidebarObjectKindsForDatabase("hbase")).toEqual(["TABLE"]);
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
|
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
|
||||||
import { buildXuguPackageMemberNodes, markXuguPackageNodesExpandable } from "@/lib/sidebar/xuguPackageMembers";
|
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
|
||||||
|
|
||||||
function packageNode(): TreeNode {
|
function packageNode(): TreeNode {
|
||||||
return {
|
return {
|
||||||
@@ -15,10 +15,10 @@ function packageNode(): TreeNode {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Xugu package member tree", () => {
|
describe("package member tree", () => {
|
||||||
it("marks package specifications as expandable without changing other objects", () => {
|
it("marks package specifications as expandable without changing other objects", () => {
|
||||||
const nodes: TreeNode[] = [packageNode(), { id: "body", label: "business_api", type: "package-body" }, { id: "proc", label: "standalone", type: "procedure" }];
|
const nodes: TreeNode[] = [packageNode(), { id: "body", label: "business_api", type: "package-body" }, { id: "proc", label: "standalone", type: "procedure" }];
|
||||||
const result = markXuguPackageNodesExpandable(nodes);
|
const result = markPackageNodesExpandable(nodes);
|
||||||
|
|
||||||
expect(result[0]?.children).toEqual([]);
|
expect(result[0]?.children).toEqual([]);
|
||||||
expect(result[1]?.children).toBeUndefined();
|
expect(result[1]?.children).toBeUndefined();
|
||||||
@@ -32,7 +32,7 @@ describe("Xugu package member tree", () => {
|
|||||||
{ name: "lookup", kind: "function", signature: null, data_type: "VARCHAR" },
|
{ name: "lookup", kind: "function", signature: null, data_type: "VARCHAR" },
|
||||||
{ name: "ignored_table", kind: "table" },
|
{ name: "ignored_table", kind: "table" },
|
||||||
];
|
];
|
||||||
const result = buildXuguPackageMemberNodes(packageNode(), candidates);
|
const result = buildPackageMemberNodes(packageNode(), candidates);
|
||||||
|
|
||||||
expect(result.map((node) => [node.type, node.label])).toEqual([
|
expect(result.map((node) => [node.type, node.label])).toEqual([
|
||||||
["procedure", "calculate(p_value IN INT)"],
|
["procedure", "calculate(p_value IN INT)"],
|
||||||
@@ -50,7 +50,7 @@ describe("Xugu package member tree", () => {
|
|||||||
{ name: "MIXEDCASE", kind: "function", signature: "p INT" },
|
{ name: "MIXEDCASE", kind: "function", signature: "p INT" },
|
||||||
{ name: "", kind: "procedure" },
|
{ name: "", kind: "procedure" },
|
||||||
];
|
];
|
||||||
const result = buildXuguPackageMemberNodes(packageNode(), candidates);
|
const result = buildPackageMemberNodes(packageNode(), candidates);
|
||||||
|
|
||||||
expect(result.map((node) => node.objectName)).toEqual(["MixedCase", "MIXEDCASE"]);
|
expect(result.map((node) => node.objectName)).toEqual(["MixedCase", "MIXEDCASE"]);
|
||||||
});
|
});
|
||||||
@@ -18,6 +18,7 @@ const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZ
|
|||||||
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
|
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
|
||||||
const DAMENG_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"];
|
const DAMENG_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"];
|
||||||
const XUGU_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "SYNONYM", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"];
|
const XUGU_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "SYNONYM", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"];
|
||||||
|
const PACKAGE_MEMBER_EXPANSION_DATABASES = new Set<DatabaseType>(["oracle", "xugu"]);
|
||||||
|
|
||||||
const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
||||||
// postgres
|
// postgres
|
||||||
@@ -82,6 +83,10 @@ export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObj
|
|||||||
return DATABASE_TYPE_OBJECTS.get(dbType) ?? [...ROUTINE_OBJECTS];
|
return DATABASE_TYPE_OBJECTS.get(dbType) ?? [...ROUTINE_OBJECTS];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function supportsPackageMemberExpansion(dbType?: DatabaseType): boolean {
|
||||||
|
return !!dbType && PACKAGE_MEMBER_EXPANSION_DATABASES.has(dbType);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
|
export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
|
||||||
const value = type.toUpperCase();
|
const value = type.toUpperCase();
|
||||||
const normalized = value.replace(/[\s-]+/g, "_");
|
const normalized = value.replace(/[\s-]+/g, "_");
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
|
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
|
||||||
|
|
||||||
export function markXuguPackageNodesExpandable(nodes: TreeNode[]): TreeNode[] {
|
export function markPackageNodesExpandable(nodes: TreeNode[]): TreeNode[] {
|
||||||
return nodes.map((node) => (node.type === "package" ? { ...node, children: node.children ?? [] } : node));
|
return nodes.map((node) => (node.type === "package" ? { ...node, children: node.children ?? [] } : node));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildXuguPackageMemberNodes(packageNode: TreeNode, candidates: readonly CompletionAssistantCandidate[]): TreeNode[] {
|
export function buildPackageMemberNodes(packageNode: TreeNode, candidates: readonly CompletionAssistantCandidate[]): TreeNode[] {
|
||||||
const parentName = packageNode.objectName || packageNode.label;
|
const parentName = packageNode.objectName || packageNode.label;
|
||||||
const parentSchema = packageNode.schema;
|
const parentSchema = packageNode.schema;
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -178,7 +178,7 @@ describe("connectionStore metadata loading", () => {
|
|||||||
store.connectedIds = new Set([connection.id]);
|
store.connectedIds = new Set([connection.id]);
|
||||||
store.treeNodes = [packageNode];
|
store.treeNodes = [packageNode];
|
||||||
|
|
||||||
await store.loadXuguPackageMembers(packageNode);
|
await store.loadPackageMembers(packageNode);
|
||||||
|
|
||||||
expect(completionAssistantSearch).toHaveBeenCalledWith({
|
expect(completionAssistantSearch).toHaveBeenCalledWith({
|
||||||
connection_id: connection.id,
|
connection_id: connection.id,
|
||||||
@@ -200,6 +200,84 @@ describe("connectionStore metadata loading", () => {
|
|||||||
expect(packageNode.children?.every((child) => child.parentName === "business_api")).toBe(true);
|
expect(packageNode.children?.every((child) => child.parentName === "business_api")).toBe(true);
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
|
it("loads Oracle package overloads through the shared package member path", async () => {
|
||||||
|
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||||
|
candidates: [
|
||||||
|
{ name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN NUMBER" },
|
||||||
|
{ name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN VARCHAR2" },
|
||||||
|
{ name: "ITEM_COUNT", kind: "function", data_type: "NUMBER" },
|
||||||
|
],
|
||||||
|
incomplete: false,
|
||||||
|
fallback_used: false,
|
||||||
|
});
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({ checkConnectionHealth: vi.fn().mockResolvedValue(undefined), completionAssistantSearch }));
|
||||||
|
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const store = useConnectionStore();
|
||||||
|
const connection = oracleConnection();
|
||||||
|
const packageNode: TreeNode = {
|
||||||
|
id: "oracle-1:XE:APP:package:BUSINESS_API",
|
||||||
|
label: "BUSINESS_API",
|
||||||
|
type: "package",
|
||||||
|
connectionId: connection.id,
|
||||||
|
database: "XE",
|
||||||
|
schema: "APP",
|
||||||
|
children: [],
|
||||||
|
isExpanded: false,
|
||||||
|
};
|
||||||
|
store.connections = [connection];
|
||||||
|
store.connectedIds = new Set([connection.id]);
|
||||||
|
store.treeNodes = [packageNode];
|
||||||
|
|
||||||
|
await store.loadPackageMembers(packageNode);
|
||||||
|
|
||||||
|
expect(completionAssistantSearch).toHaveBeenCalledWith({
|
||||||
|
connection_id: connection.id,
|
||||||
|
database: "XE",
|
||||||
|
schema: "APP",
|
||||||
|
object_kinds: ["routine"],
|
||||||
|
mask: "",
|
||||||
|
case_sensitive: true,
|
||||||
|
global_search: false,
|
||||||
|
max_results: 1000,
|
||||||
|
search_in_comments: false,
|
||||||
|
search_in_definitions: false,
|
||||||
|
parent_schema: "APP",
|
||||||
|
parent_name: "BUSINESS_API",
|
||||||
|
match_mode: "prefix",
|
||||||
|
});
|
||||||
|
expect(packageNode.isExpanded).toBe(true);
|
||||||
|
expect(packageNode.children?.map((child) => child.label)).toEqual(["CALCULATE(P_VALUE IN NUMBER)", "CALCULATE(P_VALUE IN VARCHAR2)", "ITEM_COUNT"]);
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
it("does not query package members for unsupported databases", async () => {
|
||||||
|
const completionAssistantSearch = vi.fn();
|
||||||
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
|
vi.doMock("@/lib/backend/api", () => ({ checkConnectionHealth: vi.fn().mockResolvedValue(undefined), completionAssistantSearch }));
|
||||||
|
|
||||||
|
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||||
|
const store = useConnectionStore();
|
||||||
|
const connection = { ...xuguConnection(), id: "dameng-1", db_type: "dameng" } as ConnectionConfig;
|
||||||
|
const packageNode: TreeNode = {
|
||||||
|
id: "dameng-1:APP:package:BUSINESS_API",
|
||||||
|
label: "BUSINESS_API",
|
||||||
|
type: "package",
|
||||||
|
connectionId: connection.id,
|
||||||
|
database: "APP",
|
||||||
|
schema: "APP",
|
||||||
|
children: [],
|
||||||
|
isExpanded: false,
|
||||||
|
};
|
||||||
|
store.connections = [connection];
|
||||||
|
store.connectedIds = new Set([connection.id]);
|
||||||
|
|
||||||
|
await store.loadPackageMembers(packageNode);
|
||||||
|
|
||||||
|
expect(completionAssistantSearch).not.toHaveBeenCalled();
|
||||||
|
expect(packageNode.isExpanded).toBe(false);
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
it("loads missing database roots only for connected sidebar search targets", async () => {
|
it("loads missing database roots only for connected sidebar search targets", async () => {
|
||||||
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
|
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
|
||||||
const listDatabases = vi.fn().mockResolvedValue([{ name: "dajia", comment: null }]);
|
const listDatabases = vi.fn().mockResolvedValue([{ name: "dajia", comment: null }]);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import { mergeSqlObjectNavigationType, sqlObjectNavigationTypeFromTableType } fr
|
|||||||
import * as api from "@/lib/backend/api";
|
import * as api from "@/lib/backend/api";
|
||||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||||
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
|
||||||
import { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
|
import { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, supportsPackageMemberExpansion, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
|
||||||
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionShouldDiscoverJdbcSchemas, connectionShouldLoadIdentifierQuote, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbIdentifierQuoteOverride } from "@/lib/database/jdbcDialect";
|
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionShouldDiscoverJdbcSchemas, connectionShouldLoadIdentifierQuote, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbIdentifierQuoteOverride } from "@/lib/database/jdbcDialect";
|
||||||
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, compareSidebarNames, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
|
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, compareSidebarNames, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
|
||||||
import { buildSqlServerDatabaseTreeNodes } from "@/lib/database/sqlServerTree";
|
import { buildSqlServerDatabaseTreeNodes } from "@/lib/database/sqlServerTree";
|
||||||
@@ -111,7 +111,7 @@ import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVer
|
|||||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||||
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
|
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
|
||||||
import { filterNacosNamespacesForSidebar, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
import { filterNacosNamespacesForSidebar, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||||
import { buildXuguPackageMemberNodes, markXuguPackageNodesExpandable } from "@/lib/sidebar/xuguPackageMembers";
|
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
|
||||||
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
|
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
|
||||||
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
|
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
|
||||||
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
|
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
|
||||||
@@ -1606,7 +1606,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
});
|
});
|
||||||
const refreshedGroup = grouped.find((group) => group.type === options.node.type);
|
const refreshedGroup = grouped.find((group) => group.type === options.node.type);
|
||||||
const children = refreshedGroup?.children ?? [];
|
const children = refreshedGroup?.children ?? [];
|
||||||
return options.node.connectionId && getConfig(options.node.connectionId)?.db_type === "xugu" ? markXuguPackageNodesExpandable(children) : children;
|
const databaseType = options.node.connectionId ? effectiveDatabaseTypeForConnection(getConfig(options.node.connectionId)) : undefined;
|
||||||
|
return supportsPackageMemberExpansion(databaseType) ? markPackageNodesExpandable(children) : children;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string): SqlCompletionTable[] {
|
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string): SqlCompletionTable[] {
|
||||||
@@ -1870,8 +1871,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
schema: options.effectiveSchema,
|
schema: options.effectiveSchema,
|
||||||
objects: supplementalObjects,
|
objects: supplementalObjects,
|
||||||
});
|
});
|
||||||
if (getConfig(options.connectionId)?.db_type === "xugu") {
|
if (supportsPackageMemberExpansion(effectiveDatabaseTypeForConnection(getConfig(options.connectionId)))) {
|
||||||
supplementalChildren = markXuguPackageNodesExpandable(supplementalChildren);
|
supplementalChildren = markPackageNodesExpandable(supplementalChildren);
|
||||||
}
|
}
|
||||||
if (supplementalChildren.length === 0) return;
|
if (supplementalChildren.length === 0) return;
|
||||||
if (isTreeLoadSearchChanged(searchFilter, options.loadOptions)) return;
|
if (isTreeLoadSearchChanged(searchFilter, options.loadOptions)) return;
|
||||||
@@ -5618,8 +5619,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadXuguPackageMembers(node: TreeNode, options?: LoadTreeOptions): Promise<void> {
|
async function loadPackageMembers(node: TreeNode, options?: LoadTreeOptions): Promise<void> {
|
||||||
if (node.type !== "package" || !node.connectionId || !node.database || getConfig(node.connectionId)?.db_type !== "xugu") return;
|
if (node.type !== "package" || !node.connectionId || !node.database) return;
|
||||||
|
if (!supportsPackageMemberExpansion(effectiveDatabaseTypeForConnection(getConfig(node.connectionId)))) return;
|
||||||
const connectionId = node.connectionId;
|
const connectionId = node.connectionId;
|
||||||
const database = node.database;
|
const database = node.database;
|
||||||
const schema = node.schema;
|
const schema = node.schema;
|
||||||
@@ -5629,7 +5631,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
try {
|
try {
|
||||||
await runTreeMetadataLoad(
|
await runTreeMetadataLoad(
|
||||||
{
|
{
|
||||||
kind: "xugu-package-members",
|
kind: "package-members",
|
||||||
connectionId,
|
connectionId,
|
||||||
database,
|
database,
|
||||||
schema,
|
schema,
|
||||||
@@ -5656,7 +5658,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
});
|
});
|
||||||
const targetNode = treeNodeLoadTarget(load);
|
const targetNode = treeNodeLoadTarget(load);
|
||||||
if (!targetNode) return;
|
if (!targetNode) return;
|
||||||
setChildren(targetNode, buildXuguPackageMemberNodes(targetNode, response.candidates));
|
setChildren(targetNode, buildPackageMemberNodes(targetNode, response.candidates));
|
||||||
targetNode.isExpanded = true;
|
targetNode.isExpanded = true;
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
@@ -7280,7 +7282,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
loadTables,
|
loadTables,
|
||||||
loadTableForLocate,
|
loadTableForLocate,
|
||||||
loadObjectGroupChildren,
|
loadObjectGroupChildren,
|
||||||
loadXuguPackageMembers,
|
loadPackageMembers,
|
||||||
loadMoreObjectGroupChildren,
|
loadMoreObjectGroupChildren,
|
||||||
loadAllObjectGroupChildren,
|
loadAllObjectGroupChildren,
|
||||||
loadTableGroups,
|
loadTableGroups,
|
||||||
|
|||||||
Reference in new issue
Block a user