mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(xugu): expose public synonyms in an isolated scope
This commit is contained in:
+117
-30
@@ -28,11 +28,26 @@ const defaultMaxRows = 1000
|
||||
const defaultXuguPort = 5138
|
||||
const legacyAgentSessionID = "__legacy__"
|
||||
const maxAgentSessions = 256
|
||||
|
||||
// xuguPublicSynonymScope is a protocol-only namespace for database-global
|
||||
// synonyms. It is deliberately not a real schema name (and must never be
|
||||
// interpreted as one by metadata queries).
|
||||
const xuguPublicSynonymScope = "\x00DBX_XUGU_PUBLIC_SYNONYMS"
|
||||
const xuguListDatabasesSQL = `
|
||||
SELECT DB_NAME
|
||||
FROM ALL_DATABASES
|
||||
ORDER BY DB_NAME`
|
||||
const xuguListSchemasSQL = `
|
||||
SELECT s.SCHEMA_NAME AS SCHEMA_NAME, FALSE AS IS_PUBLIC_SCOPE
|
||||
FROM ALL_SCHEMAS s
|
||||
WHERE s.DB_ID = CURRENT_DB_ID
|
||||
UNION
|
||||
SELECT '' AS SCHEMA_NAME, TRUE AS IS_PUBLIC_SCOPE
|
||||
FROM ALL_SYNONYMS y
|
||||
WHERE y.DB_ID = CURRENT_DB_ID
|
||||
AND y.IS_PUBLIC = TRUE
|
||||
ORDER BY IS_PUBLIC_SCOPE, SCHEMA_NAME`
|
||||
const xuguListSchemasFallbackSQL = `
|
||||
SELECT SCHEMA_NAME
|
||||
FROM ALL_SCHEMAS
|
||||
WHERE DB_ID = CURRENT_DB_ID
|
||||
@@ -48,12 +63,12 @@ JOIN ALL_SCHEMAS s ON s.DB_ID = q.DB_ID AND s.SCHEMA_ID = q.SCHEMA_ID
|
||||
WHERE s.DB_ID = CURRENT_DB_ID
|
||||
AND q.IS_SYS = FALSE`
|
||||
const xuguCatalogSynonymSelectSQL = `
|
||||
SELECT s.SCHEMA_NAME, y.SYNO_NAME, t.SCHEMA_NAME AS TARGET_SCHEMA, y.TARG_NAME
|
||||
SELECT s.SCHEMA_NAME,
|
||||
y.SYNO_NAME, t.SCHEMA_NAME AS TARGET_SCHEMA, y.TARG_NAME, y.IS_PUBLIC
|
||||
FROM ALL_SYNONYMS y
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = y.DB_ID AND s.SCHEMA_ID = y.SCHEMA_ID
|
||||
LEFT JOIN ALL_SCHEMAS s ON s.DB_ID = y.DB_ID AND s.SCHEMA_ID = y.SCHEMA_ID
|
||||
LEFT JOIN ALL_SCHEMAS t ON t.DB_ID = y.DB_ID AND t.SCHEMA_ID = y.TARG_SCHE_ID
|
||||
WHERE s.DB_ID = CURRENT_DB_ID
|
||||
AND y.IS_PUBLIC = FALSE`
|
||||
WHERE y.DB_ID = CURRENT_DB_ID`
|
||||
const xuguPrimaryKeyColumnsSQL = `
|
||||
SELECT c.DEFINE
|
||||
FROM ALL_CONSTRAINTS c
|
||||
@@ -457,6 +472,7 @@ type xuguCatalogSynonym struct {
|
||||
Name string
|
||||
TargetSchema sql.NullString
|
||||
TargetName string
|
||||
Public bool
|
||||
}
|
||||
|
||||
// xuguIndexKey preserves the catalog spelling and SQL semantics of an index
|
||||
@@ -1689,21 +1705,60 @@ func xuguDSNValue(dsn string, key string) string {
|
||||
func (s *server) listSchemas() ([]string, error) {
|
||||
rows, err := s.queryRows(xuguListSchemasSQL, nil)
|
||||
if err != nil {
|
||||
if fallback := strings.ToUpper(strings.TrimSpace(s.params.Username)); fallback != "" && isXuguMetadataUnavailableError(err) {
|
||||
return []string{fallback}, nil
|
||||
rows, err = s.queryRows(xuguListSchemasFallbackSQL, nil)
|
||||
if err != nil {
|
||||
if fallback := strings.ToUpper(strings.TrimSpace(s.params.Username)); fallback != "" && isXuguMetadataUnavailableError(err) {
|
||||
return []string{fallback}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
return s.scanXuguSchemaRows(rows, false)
|
||||
}
|
||||
return s.scanXuguSchemaRows(rows, true)
|
||||
}
|
||||
|
||||
func (s *server) scanXuguSchemaRows(rows *sql.Rows, includesPublicScope bool) ([]string, error) {
|
||||
defer s.closeRows(rows)
|
||||
var result []string
|
||||
for rows.Next() {
|
||||
var schema string
|
||||
if err := rows.Scan(&schema); err != nil {
|
||||
var schema sql.NullString
|
||||
var publicScope bool
|
||||
var err error
|
||||
if includesPublicScope {
|
||||
err = rows.Scan(&schema, &publicScope)
|
||||
} else {
|
||||
err = rows.Scan(&schema)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, schema)
|
||||
if publicScope {
|
||||
result = append(result, xuguPublicSynonymScope)
|
||||
} else if schema.Valid && schema.String != "" {
|
||||
result = append(result, schema.String)
|
||||
}
|
||||
}
|
||||
return emptyIfNil(result), rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dedupeXuguSchemaNames(result), nil
|
||||
}
|
||||
|
||||
func dedupeXuguSchemaNames(names []string) []string {
|
||||
seen := make(map[string]struct{}, len(names))
|
||||
result := make([]string, 0, len(names))
|
||||
for _, name := range names {
|
||||
if _, ok := seen[name]; ok {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
result = append(result, name)
|
||||
}
|
||||
return emptyIfNil(result)
|
||||
}
|
||||
|
||||
func isXuguPublicSynonymScope(schema string) bool {
|
||||
return strings.TrimSpace(schema) == xuguPublicSynonymScope
|
||||
}
|
||||
|
||||
func (s *server) currentSchema() (string, error) {
|
||||
@@ -2160,6 +2215,24 @@ WHERE s.DB_ID = CURRENT_DB_ID
|
||||
}
|
||||
|
||||
func xuguListObjectsQuery(schema string, constraints metadataListConstraints) xuguMetadataListQuery {
|
||||
// Public synonyms have no owning schema in Xugu. They are queried only
|
||||
// through the reserved protocol scope; a real schema named GUEST remains
|
||||
// a normal private-synonym namespace.
|
||||
publicSynonymScope := isXuguPublicSynonymScope(schema)
|
||||
synonymSQL := `
|
||||
SELECT y.SYNO_NAME AS OBJECT_NAME, 'SYNONYM' AS OBJECT_TYPE, NULL AS COMMENTS, y.VALID, NULL AS XUGU_TYPE_MEMBERS_EXPANDABLE
|
||||
FROM ALL_SYNONYMS y
|
||||
WHERE y.DB_ID = CURRENT_DB_ID
|
||||
AND y.IS_PUBLIC = TRUE`
|
||||
if !publicSynonymScope {
|
||||
synonymSQL = `
|
||||
SELECT y.SYNO_NAME AS OBJECT_NAME, 'SYNONYM' AS OBJECT_TYPE, NULL AS COMMENTS, y.VALID, NULL AS XUGU_TYPE_MEMBERS_EXPANDABLE
|
||||
FROM ALL_SYNONYMS y
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = y.DB_ID AND s.SCHEMA_ID = y.SCHEMA_ID
|
||||
WHERE y.DB_ID = CURRENT_DB_ID
|
||||
AND UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
AND y.IS_PUBLIC = FALSE`
|
||||
}
|
||||
type objectSource struct {
|
||||
objectTypes []string
|
||||
sql string
|
||||
@@ -2211,13 +2284,7 @@ JOIN ALL_SCHEMAS s ON s.DB_ID = q.DB_ID AND s.SCHEMA_ID = q.SCHEMA_ID
|
||||
WHERE s.DB_ID = CURRENT_DB_ID
|
||||
AND UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
AND q.IS_SYS = FALSE`},
|
||||
{objectTypes: []string{"SYNONYM"}, sql: `
|
||||
SELECT y.SYNO_NAME AS OBJECT_NAME, 'SYNONYM' AS OBJECT_TYPE, NULL AS COMMENTS, y.VALID, NULL AS XUGU_TYPE_MEMBERS_EXPANDABLE
|
||||
FROM ALL_SYNONYMS y
|
||||
JOIN ALL_SCHEMAS s ON s.DB_ID = y.DB_ID AND s.SCHEMA_ID = y.SCHEMA_ID
|
||||
WHERE s.DB_ID = CURRENT_DB_ID
|
||||
AND UPPER(s.SCHEMA_NAME) = UPPER(?)
|
||||
AND y.IS_PUBLIC = FALSE`},
|
||||
{objectTypes: []string{"SYNONYM"}, sql: synonymSQL},
|
||||
{objectTypes: []string{"TYPE"}, sql: `
|
||||
SELECT u.TYPE_NAME AS OBJECT_NAME, 'TYPE' AS OBJECT_TYPE, u.COMMENTS, u.VALID,
|
||||
CASE WHEN u.UDT_TYPE = 1001 THEN TRUE ELSE FALSE END AS XUGU_TYPE_MEMBERS_EXPANDABLE
|
||||
@@ -2263,7 +2330,9 @@ WHERE s.DB_ID = CURRENT_DB_ID
|
||||
baseArgs := make([]any, 0, len(baseSources))
|
||||
for _, source := range baseSources {
|
||||
baseSQLParts = append(baseSQLParts, source.sql)
|
||||
baseArgs = append(baseArgs, schema)
|
||||
if !(publicSynonymScope && len(source.objectTypes) == 1 && source.objectTypes[0] == "SYNONYM") {
|
||||
baseArgs = append(baseArgs, schema)
|
||||
}
|
||||
}
|
||||
return xuguConstrainedMetadataListQuery(
|
||||
strings.Join(baseSQLParts, "\nUNION ALL\n"),
|
||||
@@ -2929,9 +2998,9 @@ func renderXuguSequenceDDL(sequence xuguSequenceMetadata) string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// getSynonymSource reconstructs private synonym DDL from ALL_SYNONYMS. Public
|
||||
// synonyms deliberately remain outside schema groups: their catalog rows have
|
||||
// no owning schema and showing them in every schema would duplicate objects.
|
||||
// getSynonymSource reconstructs synonym DDL from ALL_SYNONYMS. Public
|
||||
// synonyms are exposed in the reserved database-global scope and therefore
|
||||
// use PUBLIC syntax without a schema qualifier.
|
||||
func (s *server) getSynonymSource(schema, name string) (map[string]any, error) {
|
||||
synonym, err := s.resolveCatalogSynonym(schema, name)
|
||||
if err != nil {
|
||||
@@ -2942,10 +3011,15 @@ func (s *server) getSynonymSource(schema, name string) (map[string]any, error) {
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("CREATE SYNONYM ")
|
||||
builder.WriteString(quoteIdentifier(synonym.Schema))
|
||||
builder.WriteByte('.')
|
||||
builder.WriteString(quoteIdentifier(synonym.Name))
|
||||
if synonym.Public {
|
||||
builder.WriteString("CREATE PUBLIC SYNONYM ")
|
||||
builder.WriteString(quoteIdentifier(synonym.Name))
|
||||
} else {
|
||||
builder.WriteString("CREATE SYNONYM ")
|
||||
builder.WriteString(quoteIdentifier(synonym.Schema))
|
||||
builder.WriteByte('.')
|
||||
builder.WriteString(quoteIdentifier(synonym.Name))
|
||||
}
|
||||
builder.WriteString("\nFOR ")
|
||||
if targetSchema := strings.TrimSpace(synonym.TargetSchema.String); targetSchema != "" {
|
||||
builder.WriteString(quoteIdentifier(targetSchema))
|
||||
@@ -2996,13 +3070,20 @@ func (s *server) resolveCatalogSynonym(schema, name string) (xuguCatalogSynonym,
|
||||
func xuguCatalogSynonymQuery(schema, name string, caseInsensitive bool) string {
|
||||
schemaExpr := quoteStringLiteral(schema)
|
||||
nameExpr := quoteStringLiteral(name)
|
||||
publicScope := isXuguPublicSynonymScope(schema)
|
||||
if caseInsensitive {
|
||||
schemaExpr = quoteStringLiteral(strings.ToUpper(schema))
|
||||
nameExpr = quoteStringLiteral(strings.ToUpper(name))
|
||||
return xuguCatalogSynonymSelectSQL + "\n AND UPPER(s.SCHEMA_NAME) = " + schemaExpr +
|
||||
if publicScope {
|
||||
return xuguCatalogSynonymSelectSQL + "\n AND y.IS_PUBLIC = TRUE\n AND UPPER(y.SYNO_NAME) = " + nameExpr
|
||||
}
|
||||
schemaExpr = quoteStringLiteral(strings.ToUpper(schema))
|
||||
return xuguCatalogSynonymSelectSQL + "\n AND y.IS_PUBLIC = FALSE\n AND UPPER(s.SCHEMA_NAME) = " + schemaExpr +
|
||||
"\n AND UPPER(y.SYNO_NAME) = " + nameExpr
|
||||
}
|
||||
return xuguCatalogSynonymSelectSQL + "\n AND s.SCHEMA_NAME = " + schemaExpr +
|
||||
if publicScope {
|
||||
return xuguCatalogSynonymSelectSQL + "\n AND y.IS_PUBLIC = TRUE\n AND y.SYNO_NAME = " + nameExpr
|
||||
}
|
||||
return xuguCatalogSynonymSelectSQL + "\n AND y.IS_PUBLIC = FALSE\n AND s.SCHEMA_NAME = " + schemaExpr +
|
||||
"\n AND y.SYNO_NAME = " + nameExpr
|
||||
}
|
||||
|
||||
@@ -3016,9 +3097,15 @@ func (s *server) catalogSynonymCandidates(query string) ([]xuguCatalogSynonym, e
|
||||
var candidates []xuguCatalogSynonym
|
||||
for rows.Next() {
|
||||
var candidate xuguCatalogSynonym
|
||||
if err := rows.Scan(&candidate.Schema, &candidate.Name, &candidate.TargetSchema, &candidate.TargetName); err != nil {
|
||||
var schema sql.NullString
|
||||
if err := rows.Scan(&schema, &candidate.Name, &candidate.TargetSchema, &candidate.TargetName, &candidate.Public); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if candidate.Public {
|
||||
candidate.Schema = xuguPublicSynonymScope
|
||||
} else {
|
||||
candidate.Schema = schema.String
|
||||
}
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
return candidates, rows.Err()
|
||||
|
||||
@@ -711,11 +711,94 @@ func TestSchemaListingSQLUsesLowPrivilegeDictionary(t *testing.T) {
|
||||
if !strings.Contains(sqlText, "ALL_SCHEMAS") || strings.Contains(sqlText, "SYS_SCHEMAS") {
|
||||
t.Fatalf("schema listing should query low-privilege ALL_SCHEMAS, got: %s", xuguListSchemasSQL)
|
||||
}
|
||||
if !strings.Contains(sqlText, "ALL_SYNONYMS") || !strings.Contains(sqlText, "IS_PUBLIC_SCOPE") {
|
||||
t.Fatalf("schema listing should expose public synonyms in the same query: %s", xuguListSchemasSQL)
|
||||
}
|
||||
if !strings.Contains(sqlText, "DB_ID = CURRENT_DB_ID") {
|
||||
t.Fatalf("schema listing must be scoped to the selected database: %s", xuguListSchemasSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguListSchemasExposesPublicScopeWithoutGUESTCollision(t *testing.T) {
|
||||
db, err := sql.Open("xugu-test-schema-listing", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
realGuest bool
|
||||
realReserved bool
|
||||
public bool
|
||||
want []string
|
||||
}{
|
||||
{name: "private only", want: []string{"APP_TEST", "SYSDBA"}},
|
||||
{name: "public without real guest", public: true, want: []string{"APP_TEST", "SYSDBA", xuguPublicSynonymScope}},
|
||||
{name: "public with real guest", realGuest: true, public: true, want: []string{"APP_TEST", "GUEST", "SYSDBA", xuguPublicSynonymScope}},
|
||||
{name: "public with former reserved schema", realReserved: true, public: true, want: []string{"APP_TEST", "__DBX_XUGU_PUBLIC_SYNONYMS__", "SYSDBA", xuguPublicSynonymScope}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
xuguSchemaListingState.Lock()
|
||||
xuguSchemaListingState.realGuest = tc.realGuest
|
||||
xuguSchemaListingState.realReserved = tc.realReserved
|
||||
xuguSchemaListingState.public = tc.public
|
||||
xuguSchemaListingState.combinedUnavailable = false
|
||||
xuguSchemaListingState.queryCount = 0
|
||||
xuguSchemaListingState.Unlock()
|
||||
|
||||
s := newServer()
|
||||
s.db = db
|
||||
got, err := s.listSchemas()
|
||||
if err != nil {
|
||||
t.Fatalf("listSchemas() error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("listSchemas() = %v, want %v", got, tc.want)
|
||||
}
|
||||
xuguSchemaListingState.Lock()
|
||||
queryCount := xuguSchemaListingState.queryCount
|
||||
xuguSchemaListingState.Unlock()
|
||||
if queryCount != 1 {
|
||||
t.Fatalf("listSchemas() made %d metadata requests, want 1", queryCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguListSchemasFallsBackWhenCombinedQueryIsUnavailable(t *testing.T) {
|
||||
db, err := sql.Open("xugu-test-schema-listing", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
xuguSchemaListingState.Lock()
|
||||
xuguSchemaListingState.realGuest = true
|
||||
xuguSchemaListingState.realReserved = false
|
||||
xuguSchemaListingState.public = true
|
||||
xuguSchemaListingState.combinedUnavailable = true
|
||||
xuguSchemaListingState.queryCount = 0
|
||||
xuguSchemaListingState.Unlock()
|
||||
|
||||
s := newServer()
|
||||
s.db = db
|
||||
got, err := s.listSchemas()
|
||||
if err != nil {
|
||||
t.Fatalf("listSchemas() error: %v", err)
|
||||
}
|
||||
if want := []string{"APP_TEST", "GUEST", "SYSDBA"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("listSchemas() = %v, want %v", got, want)
|
||||
}
|
||||
xuguSchemaListingState.Lock()
|
||||
queryCount := xuguSchemaListingState.queryCount
|
||||
xuguSchemaListingState.Unlock()
|
||||
if queryCount != 2 {
|
||||
t.Fatalf("fallback listSchemas() made %d metadata requests, want 2", queryCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguMetadataQueriesAreCurrentDatabaseScoped(t *testing.T) {
|
||||
queries := map[string]string{
|
||||
"schemas": xuguListSchemasSQL,
|
||||
@@ -1131,6 +1214,46 @@ func TestAvailableXuguObjectTypesRespectsConstraints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguListObjectsQueryExposesPublicSynonymsOnlyInReservedScope(t *testing.T) {
|
||||
query := xuguListObjectsQuery(xuguPublicSynonymScope, metadataListConstraints{ObjectTypes: []string{"SYNONYM"}})
|
||||
upper := strings.ToUpper(query.SQL)
|
||||
for _, want := range []string{"FROM ALL_SYNONYMS Y", "Y.IS_PUBLIC = TRUE", "OBJECT_TYPE IN (?)"} {
|
||||
if !strings.Contains(upper, want) {
|
||||
t.Fatalf("public synonym query is missing %q:\n%s", want, query.SQL)
|
||||
}
|
||||
}
|
||||
synonymStart := strings.Index(upper, "SELECT Y.SYNO_NAME")
|
||||
synonymEnd := strings.Index(upper[synonymStart:], "UNION ALL")
|
||||
if synonymStart < 0 {
|
||||
t.Fatalf("public synonym branch could not be isolated:\n%s", query.SQL)
|
||||
}
|
||||
if synonymEnd < 0 {
|
||||
synonymEnd = len(upper) - synonymStart
|
||||
}
|
||||
synonymBranch := upper[synonymStart : synonymStart+synonymEnd]
|
||||
if strings.Contains(synonymBranch, "JOIN ALL_SCHEMAS") || strings.Contains(synonymBranch, "UPPER(S.SCHEMA_NAME)") {
|
||||
t.Fatalf("public synonym query must not require an owning schema:\n%s", query.SQL)
|
||||
}
|
||||
assertArgs(t, query.Args, []any{"SYNONYM"})
|
||||
|
||||
private := xuguListObjectsQuery("SYSDBA", metadataListConstraints{ObjectTypes: []string{"SYNONYM"}})
|
||||
privateUpper := strings.ToUpper(private.SQL)
|
||||
if !strings.Contains(privateUpper, "Y.IS_PUBLIC = FALSE") || !strings.Contains(privateUpper, "JOIN ALL_SCHEMAS") {
|
||||
t.Fatalf("private synonym query must remain schema-scoped:\n%s", private.SQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguListObjectsQueryKeepsRealGuestSchemaPrivate(t *testing.T) {
|
||||
query := xuguListObjectsQuery("GUEST", metadataListConstraints{ObjectTypes: []string{"SYNONYM"}})
|
||||
upper := strings.ToUpper(query.SQL)
|
||||
if !strings.Contains(upper, "Y.IS_PUBLIC = FALSE") || !strings.Contains(upper, "JOIN ALL_SCHEMAS") {
|
||||
t.Fatalf("real GUEST schema must use the private synonym query: %s", query.SQL)
|
||||
}
|
||||
if strings.Contains(upper, "Y.IS_PUBLIC = TRUE") {
|
||||
t.Fatalf("real GUEST schema must not use the public synonym scope: %s", query.SQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguListObjectsQueryExcludesSystemSequences(t *testing.T) {
|
||||
query := xuguListObjectsQuery("APP", metadataListConstraints{
|
||||
ObjectTypes: []string{"sequence"},
|
||||
@@ -1208,6 +1331,72 @@ func TestGetSynonymSourceReconstructsPrivateQuotedDDL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSynonymSourceReconstructsPublicDDLWithoutSyntheticSchema(t *testing.T) {
|
||||
db, err := sql.Open("xugu-test-public-synonym-source", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
s := newServer()
|
||||
s.db = db
|
||||
source, err := s.getObjectSource(xuguPublicSynonymScope, "DbxPublicMixed", "SYNONYM")
|
||||
if err != nil {
|
||||
t.Fatalf("get public synonym source: %v", err)
|
||||
}
|
||||
if source["schema"] != xuguPublicSynonymScope || source["name"] != "DbxPublicMixed" {
|
||||
t.Fatalf("public synonym source must preserve synthetic scope and catalog spelling: %#v", source)
|
||||
}
|
||||
ddl, _ := source["source"].(string)
|
||||
want := "CREATE PUBLIC SYNONYM \"DbxPublicMixed\"\nFOR \"SYSDBA\".\"SHOP_USERS\";"
|
||||
if ddl != want {
|
||||
t.Fatalf("public synonym DDL = %q, want %q", ddl, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguCatalogSynonymQueryUsesReservedPublicScope(t *testing.T) {
|
||||
exact := strings.ToUpper(xuguCatalogSynonymQuery(xuguPublicSynonymScope, "DbxPublicMixed", false))
|
||||
if !strings.Contains(exact, "Y.IS_PUBLIC = TRUE") || !strings.Contains(exact, "Y.SYNO_NAME = 'DBXPUBLICMIXED'") {
|
||||
t.Fatalf("exact public synonym lookup must be global and exact:\n%s", exact)
|
||||
}
|
||||
if strings.Contains(exact, "S.SCHEMA_NAME =") {
|
||||
t.Fatalf("exact public synonym lookup must not require an owning schema:\n%s", exact)
|
||||
}
|
||||
|
||||
folded := strings.ToUpper(xuguCatalogSynonymQuery(xuguPublicSynonymScope, "dbxpublicmixed", true))
|
||||
if !strings.Contains(folded, "Y.IS_PUBLIC = TRUE") || !strings.Contains(folded, "UPPER(Y.SYNO_NAME) = 'DBXPUBLICMIXED'") {
|
||||
t.Fatalf("case-insensitive public synonym lookup must remain global:\n%s", folded)
|
||||
}
|
||||
if strings.Contains(folded, "S.SCHEMA_NAME =") {
|
||||
t.Fatalf("case-insensitive public synonym lookup must not require an owning schema:\n%s", folded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguCatalogSynonymQueryTreatsRealGuestAsPrivate(t *testing.T) {
|
||||
query := strings.ToUpper(xuguCatalogSynonymQuery("GUEST", "DbxPrivateMixed", false))
|
||||
if !strings.Contains(query, "Y.IS_PUBLIC = FALSE") || !strings.Contains(query, "S.SCHEMA_NAME = 'GUEST'") {
|
||||
t.Fatalf("real GUEST schema must use private exact lookup: %s", query)
|
||||
}
|
||||
if strings.Contains(query, "AND Y.IS_PUBLIC = TRUE") {
|
||||
t.Fatalf("real GUEST schema must not use public lookup: %s", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectXuguCatalogSynonymDisambiguatesPrivateAndPublicSameName(t *testing.T) {
|
||||
candidates := []xuguCatalogSynonym{
|
||||
{Schema: "GUEST", Name: "SharedAlias", TargetSchema: sql.NullString{String: "GUEST", Valid: true}, TargetName: "PRIVATE_TARGET", Public: false},
|
||||
{Schema: xuguPublicSynonymScope, Name: "SharedAlias", TargetSchema: sql.NullString{String: "SYSDBA", Valid: true}, TargetName: "PUBLIC_TARGET", Public: true},
|
||||
}
|
||||
private, err := selectXuguCatalogSynonym("GUEST", "SharedAlias", candidates)
|
||||
if err != nil || private.Public || private.TargetName != "PRIVATE_TARGET" {
|
||||
t.Fatalf("private same-name synonym resolved incorrectly: %#v, err=%v", private, err)
|
||||
}
|
||||
public, err := selectXuguCatalogSynonym(xuguPublicSynonymScope, "SharedAlias", candidates)
|
||||
if err != nil || !public.Public || public.TargetName != "PUBLIC_TARGET" {
|
||||
t.Fatalf("public same-name synonym resolved incorrectly: %#v, err=%v", public, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderXuguSequenceDDLUsesNoCacheAndNoCycle(t *testing.T) {
|
||||
ddl := renderXuguSequenceDDL(xuguSequenceMetadata{
|
||||
Schema: "APP", Name: "SEQ_DEFAULTS", Current: int64(1), Minimum: int64(1),
|
||||
@@ -2204,10 +2393,77 @@ func init() {
|
||||
sql.Register("xugu-test-show-result", &xuguShowResultDriver{})
|
||||
sql.Register("xugu-test-sequence-source", &xuguSequenceSourceDriver{})
|
||||
sql.Register("xugu-test-synonym-source", &xuguSynonymSourceDriver{})
|
||||
sql.Register("xugu-test-public-synonym-source", &xuguPublicSynonymSourceDriver{})
|
||||
sql.Register("xugu-test-permission-metadata", &xuguPermissionMetadataDriver{})
|
||||
sql.Register("xugu-test-fallback-errors", &xuguFallbackErrorDriver{})
|
||||
sql.Register("xugu-test-eof", &xuguEOFDriver{})
|
||||
sql.Register("xugu-test-trigger-details", &xuguTriggerDetailsDriver{})
|
||||
sql.Register("xugu-test-schema-listing", &xuguSchemaListingDriver{})
|
||||
}
|
||||
|
||||
var xuguSchemaListingState struct {
|
||||
sync.Mutex
|
||||
realGuest bool
|
||||
realReserved bool
|
||||
public bool
|
||||
combinedUnavailable bool
|
||||
queryCount int
|
||||
}
|
||||
|
||||
type xuguSchemaListingDriver struct{}
|
||||
|
||||
func (d *xuguSchemaListingDriver) Open(name string) (driver.Conn, error) {
|
||||
return &xuguSchemaListingConn{}, nil
|
||||
}
|
||||
|
||||
type xuguSchemaListingConn struct{}
|
||||
|
||||
func (c *xuguSchemaListingConn) Prepare(query string) (driver.Stmt, error) {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
func (c *xuguSchemaListingConn) Close() error { return nil }
|
||||
func (c *xuguSchemaListingConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
|
||||
func (c *xuguSchemaListingConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
|
||||
upper := strings.ToUpper(query)
|
||||
xuguSchemaListingState.Lock()
|
||||
realGuest, realReserved := xuguSchemaListingState.realGuest, xuguSchemaListingState.realReserved
|
||||
public, combinedUnavailable := xuguSchemaListingState.public, xuguSchemaListingState.combinedUnavailable
|
||||
xuguSchemaListingState.queryCount++
|
||||
xuguSchemaListingState.Unlock()
|
||||
switch {
|
||||
case strings.Contains(upper, "FROM ALL_SCHEMAS") && strings.Contains(upper, "FROM ALL_SYNONYMS"):
|
||||
if combinedUnavailable {
|
||||
return nil, errors.New("combined public synonym scope query is unavailable")
|
||||
}
|
||||
values := [][]driver.Value{{"APP_TEST"}}
|
||||
if realGuest {
|
||||
values = append(values, []driver.Value{"GUEST"})
|
||||
}
|
||||
if realReserved {
|
||||
values = append(values, []driver.Value{"__DBX_XUGU_PUBLIC_SYNONYMS__"})
|
||||
}
|
||||
values = append(values, []driver.Value{"SYSDBA"})
|
||||
combinedValues := make([][]driver.Value, 0, len(values)+1)
|
||||
for _, value := range values {
|
||||
combinedValues = append(combinedValues, []driver.Value{value[0], false})
|
||||
}
|
||||
if public {
|
||||
combinedValues = append(combinedValues, []driver.Value{"", true})
|
||||
}
|
||||
return &xuguStaticRows{columns: []string{"SCHEMA_NAME", "IS_PUBLIC_SCOPE"}, values: combinedValues}, nil
|
||||
case strings.Contains(upper, "FROM ALL_SCHEMAS"):
|
||||
values := [][]driver.Value{{"APP_TEST"}}
|
||||
if realGuest {
|
||||
values = append(values, []driver.Value{"GUEST"})
|
||||
}
|
||||
if realReserved {
|
||||
values = append(values, []driver.Value{"__DBX_XUGU_PUBLIC_SYNONYMS__"})
|
||||
}
|
||||
values = append(values, []driver.Value{"SYSDBA"})
|
||||
return &xuguStaticRows{columns: []string{"SCHEMA_NAME"}, values: values}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected schema listing query: %s", query)
|
||||
}
|
||||
}
|
||||
|
||||
type xuguEOFDriver struct{}
|
||||
@@ -2656,8 +2912,37 @@ func (c *xuguSynonymSourceConn) QueryContext(_ context.Context, query string, _
|
||||
return nil, fmt.Errorf("synonym resolution must prioritize exact catalog identifiers: %s", query)
|
||||
}
|
||||
return &xuguStaticRows{
|
||||
columns: []string{"SCHEMA_NAME", "SYNO_NAME", "TARGET_SCHEMA", "TARG_NAME"},
|
||||
values: [][]driver.Value{{"SYSDBA", "dbxSynonymReplayCase", "AppSchema", "tbUserProfile"}},
|
||||
columns: []string{"SCHEMA_NAME", "SYNO_NAME", "TARGET_SCHEMA", "TARG_NAME", "IS_PUBLIC"},
|
||||
values: [][]driver.Value{{"SYSDBA", "dbxSynonymReplayCase", "AppSchema", "tbUserProfile", false}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type xuguPublicSynonymSourceDriver struct{}
|
||||
|
||||
func (d *xuguPublicSynonymSourceDriver) Open(name string) (driver.Conn, error) {
|
||||
return &xuguPublicSynonymSourceConn{}, nil
|
||||
}
|
||||
|
||||
type xuguPublicSynonymSourceConn struct{}
|
||||
|
||||
func (c *xuguPublicSynonymSourceConn) Prepare(query string) (driver.Stmt, error) {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
func (c *xuguPublicSynonymSourceConn) Close() error { return nil }
|
||||
func (c *xuguPublicSynonymSourceConn) Begin() (driver.Tx, error) {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
func (c *xuguPublicSynonymSourceConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
|
||||
upper := strings.ToUpper(query)
|
||||
if !strings.Contains(upper, "FROM ALL_SYNONYMS") || !strings.Contains(upper, "Y.IS_PUBLIC = TRUE") {
|
||||
return nil, fmt.Errorf("unexpected public synonym source query: %s", query)
|
||||
}
|
||||
if strings.Contains(upper, "S.SCHEMA_NAME =") || strings.Contains(upper, "UPPER(") || !strings.Contains(query, "y.SYNO_NAME = 'DbxPublicMixed'") {
|
||||
return nil, fmt.Errorf("public synonym resolution must use the global exact lookup: %s", query)
|
||||
}
|
||||
return &xuguStaticRows{
|
||||
columns: []string{"SCHEMA_NAME", "SYNO_NAME", "TARGET_SCHEMA", "TARG_NAME", "IS_PUBLIC"},
|
||||
values: [][]driver.Value{{nil, "DbxPublicMixed", "SYSDBA", "SHOP_USERS", true}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLivePublicSynonymScope exercises the complete discovery/list/source
|
||||
// protocol against a real Xugu database. It is opt-in because public CI does
|
||||
// not provide a Xugu service. Set XUGU_LIVE_TEST=1 and the XUGU_LIVE_*
|
||||
// connection variables to run it.
|
||||
func TestLivePublicSynonymScope(t *testing.T) {
|
||||
if os.Getenv("XUGU_LIVE_TEST") != "1" {
|
||||
t.Skip("set XUGU_LIVE_TEST=1 with XUGU_LIVE_* connection settings to run the XuguDB integration test")
|
||||
}
|
||||
params := connectParams{
|
||||
Host: os.Getenv("XUGU_LIVE_HOST"),
|
||||
Port: parsePort(os.Getenv("XUGU_LIVE_PORT")),
|
||||
Database: os.Getenv("XUGU_LIVE_DATABASE"),
|
||||
Username: os.Getenv("XUGU_LIVE_USERNAME"),
|
||||
Password: os.Getenv("XUGU_LIVE_PASSWORD"),
|
||||
}
|
||||
if params.Host == "" || params.Database == "" || params.Username == "" || params.Password == "" {
|
||||
t.Fatal("XUGU_LIVE_HOST, XUGU_LIVE_DATABASE, XUGU_LIVE_USERNAME, and XUGU_LIVE_PASSWORD are required")
|
||||
}
|
||||
|
||||
s := newServer()
|
||||
db, err := openDB(params)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.db = db
|
||||
s.params = params
|
||||
s.currentDatabase = params.Database
|
||||
defer s.disconnect()
|
||||
|
||||
// A real GUEST schema verifies that the database-global scope does not
|
||||
// replace or hide an ordinary schema in discovery. The unit matrix below
|
||||
// covers the same-name private/public routing independently of permissions
|
||||
// available in the live account.
|
||||
schemas, err := s.listSchemas()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsXuguName(schemas, "GUEST") {
|
||||
t.Skip("the live database has no real GUEST schema")
|
||||
}
|
||||
|
||||
const privateTable = "DBX_PUBLIC_SYNONYM_SCOPE_PRIVATE_T"
|
||||
const publicTable = "DBX_PUBLIC_SYNONYM_SCOPE_PUBLIC_T"
|
||||
const privateName = "DBX_PUBLIC_SYNONYM_SCOPE_PRIVATE_ALIAS"
|
||||
const publicName = "DBX_PUBLIC_SYNONYM_SCOPE_PUBLIC_ALIAS"
|
||||
privateScope := params.Username
|
||||
cleanup := []string{
|
||||
"DROP PUBLIC SYNONYM " + publicName,
|
||||
"DROP SYNONYM \"" + privateScope + "\".\"" + privateName + "\"",
|
||||
"DROP TABLE " + publicTable,
|
||||
"DROP TABLE " + privateTable,
|
||||
}
|
||||
for _, statement := range cleanup {
|
||||
_ = s.execWithReconnect(statement)
|
||||
}
|
||||
defer func() {
|
||||
for _, statement := range cleanup {
|
||||
_ = s.execWithReconnect(statement)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, statement := range []string{
|
||||
"CREATE TABLE " + privateTable + " (ID INTEGER)",
|
||||
"CREATE TABLE " + publicTable + " (ID INTEGER)",
|
||||
"CREATE OR REPLACE SYNONYM \"" + privateScope + "\".\"" + privateName + "\" FOR \"SYSDBA\".\"" + privateTable + "\"",
|
||||
"CREATE OR REPLACE PUBLIC SYNONYM " + publicName + " FOR " + publicTable,
|
||||
} {
|
||||
if err := s.execWithReconnect(statement); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
listedSchemas, err := s.listSchemas()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsXuguName(listedSchemas, "GUEST") || !containsXuguName(listedSchemas, xuguPublicSynonymScope) {
|
||||
t.Fatalf("schema discovery lost one of the independent scopes: %v", listedSchemas)
|
||||
}
|
||||
|
||||
privateObjects, err := s.listObjects(privateScope, metadataListConstraints{ObjectTypes: []string{"SYNONYM"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicObjects, err := s.listObjects(xuguPublicSynonymScope, metadataListConstraints{ObjectTypes: []string{"SYNONYM"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsXuguObject(privateObjects, privateName) {
|
||||
t.Fatalf("private scope did not expose the expected synonym: %#v", privateObjects)
|
||||
}
|
||||
if !containsXuguObject(publicObjects, publicName) {
|
||||
t.Fatalf("public scope did not expose the expected synonym: %#v", publicObjects)
|
||||
}
|
||||
|
||||
privateSource, err := s.getObjectSource(privateScope, privateName, "SYNONYM")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
publicSource, err := s.getObjectSource(xuguPublicSynonymScope, publicName, "SYNONYM")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privateDDL := privateSource["source"].(string)
|
||||
publicDDL := publicSource["source"].(string)
|
||||
if !strings.HasPrefix(privateDDL, "CREATE SYNONYM") || !strings.Contains(privateDDL, privateTable) {
|
||||
t.Fatalf("private synonym source used the wrong DDL: %#v", privateSource)
|
||||
}
|
||||
if !strings.HasPrefix(publicDDL, "CREATE PUBLIC SYNONYM") || !strings.Contains(publicDDL, publicTable) {
|
||||
t.Fatalf("public synonym source used the wrong DDL: %#v", publicSource)
|
||||
}
|
||||
}
|
||||
|
||||
func containsXuguName(names []string, want string) bool {
|
||||
for _, name := range names {
|
||||
if strings.EqualFold(name, want) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsXuguObject(objects []objectInfo, want string) bool {
|
||||
for _, object := range objects {
|
||||
if object.Name == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -9,7 +9,6 @@ gbase8a native-pushdown java-sql Uses information_schema with type, filter, stab
|
||||
gbase8s native-pushdown java-sql Uses systables with type, filter, stable order, and SKIP/FIRST.
|
||||
goldendb native-pushdown java-sql Uses information_schema with type, filter, stable order, and LIMIT/OFFSET.
|
||||
h2 native-pushdown java-sql Uses INFORMATION_SCHEMA with type, filter, stable order, and LIMIT/OFFSET.
|
||||
hive intentional-fallback java-sql Uses Hive metadata paths where stable server-side fuzzy paging is not portable, common constraints filter locally.
|
||||
hive-go shared-fallback native-go Uses HiveServer2 metadata APIs, then applies stable filtering and paging in the native agent with a SHOW TABLES fallback.
|
||||
informix native-pushdown java-sql Uses systables/sysprocedures with type, filter, stable order, and SKIP/FIRST.
|
||||
iotdb shared-fallback native-go Uses SHOW DEVICES or SHOW TABLES DETAILS for Tree/Table metadata, then applies stable filtering and paging in the native agent.
|
||||
|
||||
|
@@ -1226,7 +1226,7 @@ function findDatabaseNode(nodes: TreeNode[], connId: string, database: string):
|
||||
|
||||
function findSchemaNode(nodes: TreeNode[], connId: string, database: string, schema: string): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "schema" && node.connectionId === connId && sameTreeName(node.database, database) && sameTreeName(node.label, schema)) {
|
||||
if (node.type === "schema" && node.connectionId === connId && sameTreeName(node.database, database) && sameTreeName(node.schema || node.label, schema)) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
|
||||
@@ -73,6 +73,7 @@ import { sidebarConnectionVisibleFilterMenu } from "@/lib/sidebar/sidebarVisible
|
||||
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
||||
import { isXuguPublicSynonymTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
||||
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
|
||||
@@ -2578,7 +2579,7 @@ const canCreateSchema = computed(() => {
|
||||
const canDropSchema = computed(() => {
|
||||
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
||||
const dbType = effectiveDatabaseTypeForConnection(config);
|
||||
return activeNode.value.type === "schema" && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
return activeNode.value.type === "schema" && !isXuguPublicSynonymTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
});
|
||||
|
||||
const canEditSchemaComment = computed(() => {
|
||||
@@ -3228,6 +3229,9 @@ async function confirmCreateSchema() {
|
||||
}
|
||||
|
||||
function dropSchema() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
||||
if (isXuguPublicSynonymTreeNode(config?.db_type, node.type, node.schema)) return;
|
||||
void refreshDropSchemaPreviewSql();
|
||||
showDropSchemaConfirm.value = true;
|
||||
}
|
||||
@@ -3236,8 +3240,9 @@ async function confirmDropSchema() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (isXuguPublicSynonymTreeNode(config?.db_type, node.type, node.schema)) return;
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const dbType = effectiveDatabaseTypeForConnection(config);
|
||||
let dropExecutionSchema: string | undefined;
|
||||
if (dbType === "dameng") {
|
||||
@@ -4309,6 +4314,17 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items } = context;
|
||||
// 4. Database / Schema
|
||||
if (node.type === "database" || node.type === "schema") {
|
||||
if (isXuguPublicSynonymTreeNode(currentDatabaseType(), node.type, node.schema)) {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: t("contextMenu.refreshChildren"),
|
||||
action: refresh,
|
||||
icon: RefreshCw,
|
||||
shortcut: shortcutRefresh,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (currentDatabaseType() === "hbase") {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
if (canCreateTable.value) {
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const runtimeSource = readFileSync(fileURLToPath(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url)), "utf8");
|
||||
|
||||
function functionSource(name: string): string {
|
||||
const start = runtimeSource.indexOf(`function ${name}(`);
|
||||
expect(start, `${name} not found`).toBeGreaterThan(-1);
|
||||
const next = runtimeSource.indexOf("\nfunction ", start + 1);
|
||||
return runtimeSource.slice(start, next === -1 ? undefined : next);
|
||||
}
|
||||
|
||||
describe("Xugu public synonym actions", () => {
|
||||
it("returns a read-only schema menu before default and destructive actions", () => {
|
||||
const menu = functionSource("buildDatabaseSidebarMenu");
|
||||
const readOnlyGuard = menu.indexOf("isXuguPublicSynonymTreeNode");
|
||||
|
||||
expect(readOnlyGuard).toBeGreaterThan(-1);
|
||||
expect(readOnlyGuard).toBeLessThan(menu.indexOf("contextMenu.setDefaultSchema"));
|
||||
expect(readOnlyGuard).toBeLessThan(menu.indexOf("contextMenu.dropSchema"));
|
||||
});
|
||||
|
||||
it("guards both the drop request and confirmed execution", () => {
|
||||
expect(functionSource("dropSchema")).toContain("isXuguPublicSynonymTreeNode");
|
||||
expect(functionSource("confirmDropSchema")).toContain("isXuguPublicSynonymTreeNode");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isXuguPublicSynonymScope, isXuguPublicSynonymTreeNode, XUGU_PUBLIC_SYNONYM_SCOPE, xuguSchemaDisplayName } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
|
||||
describe("Xugu public synonym scope", () => {
|
||||
it("uses an independent protocol identity", () => {
|
||||
expect(XUGU_PUBLIC_SYNONYM_SCOPE).not.toBe("GUEST");
|
||||
expect(XUGU_PUBLIC_SYNONYM_SCOPE.startsWith("\u0000")).toBe(true);
|
||||
expect(isXuguPublicSynonymScope(XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(true);
|
||||
expect(isXuguPublicSynonymScope("GUEST")).toBe(false);
|
||||
expect(isXuguPublicSynonymScope("__DBX_XUGU_PUBLIC_SYNONYMS__")).toBe(false);
|
||||
});
|
||||
|
||||
it("only replaces the reserved key for display", () => {
|
||||
expect(xuguSchemaDisplayName(XUGU_PUBLIC_SYNONYM_SCOPE)).toBe("Public synonyms");
|
||||
expect(xuguSchemaDisplayName("GUEST")).toBe("GUEST");
|
||||
expect(xuguSchemaDisplayName("AppSchema")).toBe("AppSchema");
|
||||
});
|
||||
|
||||
it("identifies only the synthetic Xugu schema tree node", () => {
|
||||
expect(isXuguPublicSynonymTreeNode("xugu", "schema", XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(true);
|
||||
expect(isXuguPublicSynonymTreeNode("xugu", "schema", "__DBX_XUGU_PUBLIC_SYNONYMS__")).toBe(false);
|
||||
expect(isXuguPublicSynonymTreeNode("postgres", "schema", XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(false);
|
||||
expect(isXuguPublicSynonymTreeNode("xugu", "database", XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Xugu public synonyms do not belong to a user schema. The agent exposes
|
||||
* them through this reserved protocol scope so the schema tree can keep a
|
||||
* real schema named GUEST independent from database-global aliases.
|
||||
*/
|
||||
export const XUGU_PUBLIC_SYNONYM_SCOPE = "\u0000DBX_XUGU_PUBLIC_SYNONYMS";
|
||||
export const XUGU_PUBLIC_SYNONYM_SCOPE_LABEL = "Public synonyms";
|
||||
|
||||
export function isXuguPublicSynonymScope(schema: string | null | undefined): boolean {
|
||||
return schema === XUGU_PUBLIC_SYNONYM_SCOPE;
|
||||
}
|
||||
|
||||
export function xuguSchemaDisplayName(schema: string): string {
|
||||
return isXuguPublicSynonymScope(schema) ? XUGU_PUBLIC_SYNONYM_SCOPE_LABEL : schema;
|
||||
}
|
||||
|
||||
export function isXuguPublicSynonymTreeNode(databaseType: string | null | undefined, nodeType: string, schema: string | null | undefined): boolean {
|
||||
return databaseType === "xugu" && nodeType === "schema" && isXuguPublicSynonymScope(schema);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||
import { XUGU_PUBLIC_SYNONYM_SCOPE } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
@@ -24,6 +25,18 @@ function postgresConnection(): ConnectionConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function xuguConnection(): ConnectionConfig {
|
||||
return {
|
||||
...postgresConnection(),
|
||||
id: "xugu-1",
|
||||
name: "Xugu",
|
||||
db_type: "xugu",
|
||||
port: 5138,
|
||||
username: "SYSDBA",
|
||||
database: "SYSTEM",
|
||||
};
|
||||
}
|
||||
|
||||
describe("connectionStore default schema", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
@@ -86,4 +99,26 @@ describe("connectionStore default schema", () => {
|
||||
expect(schemaLabels()).toEqual(["archive", "public"]);
|
||||
expect(saveConnections).toHaveBeenLastCalledWith([expect.objectContaining({ id: connection.id, default_schema: undefined })]);
|
||||
});
|
||||
|
||||
it("does not persist the synthetic Xugu public-synonym scope as a default schema", async () => {
|
||||
const saveConnections = vi.fn().mockResolvedValue(undefined);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections,
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = xuguConnection();
|
||||
store.addEphemeralConnection(connection);
|
||||
|
||||
await store.setDefaultSchema(connection.id, XUGU_PUBLIC_SYNONYM_SCOPE);
|
||||
|
||||
expect(store.getConfig(connection.id)?.default_schema).toBeUndefined();
|
||||
expect(saveConnections).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,7 @@ import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVer
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import { buildXuguTypeMemberNodes, isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
||||
import { isXuguPublicSynonymScope, xuguSchemaDisplayName, XUGU_PUBLIC_SYNONYM_SCOPE } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
import { filterNacosNamespacesForSidebar, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
|
||||
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
|
||||
@@ -2623,7 +2624,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function setDefaultSchema(connectionId: string, schema: string) {
|
||||
const config = getConfig(connectionId);
|
||||
const defaultSchema = schema.trim();
|
||||
if (!config || !defaultSchema || config.default_schema === defaultSchema) return;
|
||||
if (!config || !defaultSchema || config.default_schema === defaultSchema || (config.db_type === "xugu" && isXuguPublicSynonymScope(defaultSchema))) return;
|
||||
await updateConnection({
|
||||
...config,
|
||||
default_schema: defaultSchema,
|
||||
@@ -3947,7 +3948,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const config = getConfig(connectionId);
|
||||
const showSystemSchemas = config?.show_system_schemas === true;
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, "schemas-v3");
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, config?.db_type === "xugu" ? "schemas-v4" : "schemas-v3");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, cacheVersion, showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
@@ -3966,13 +3967,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
{ showSystemSchemas },
|
||||
),
|
||||
);
|
||||
// The public-synonym scope is a protocol namespace, not a user
|
||||
// schema. Keep it discoverable even when a visible-schema filter is
|
||||
// configured, while preserving the raw key for object routing.
|
||||
if (config?.db_type === "xugu" && schemas.some((schema) => isXuguPublicSynonymScope(schema.name))) {
|
||||
visibleSchemaNames.add(XUGU_PUBLIC_SYNONYM_SCOPE);
|
||||
}
|
||||
const children: TreeNode[] = schemas
|
||||
.filter((schema) => visibleSchemaNames.has(schema.name))
|
||||
.map((schema) => {
|
||||
const s = schema.name;
|
||||
return {
|
||||
id: `${connectionId}:${database}:${s}`,
|
||||
label: s,
|
||||
label: config?.db_type === "xugu" ? xuguSchemaDisplayName(s) : s,
|
||||
type: "schema" as const,
|
||||
connectionId,
|
||||
database,
|
||||
|
||||
Reference in New Issue
Block a user