feat(xugu): preserve index partition metadata

This commit is contained in:
Elias
2026-08-20 15:13:07 +08:00
committed by GitHub
parent d53c855b96
commit dcbd034afa
3 changed files with 645 additions and 6 deletions
@@ -0,0 +1,197 @@
package main
import (
"os"
"strings"
"testing"
)
// TestLiveXuguIndexPartitionDDL verifies the four index cases that are easy to
// conflate in reconstructed DDL: an ordinary index, a LOCAL index, a GLOBAL
// index with explicit partitions, and a GLOBAL index with subpartitions. It is
// opt-in because CI does not provide a XuguDB service.
func TestLiveXuguIndexPartitionDDL(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()
const (
ordinaryTable = "DBX_IDX_SCOPE_LIVE_ORD_T"
localTable = "DBX_IDX_SCOPE_LIVE_LOC_T"
globalTable = "DBX_IDX_SCOPE_LIVE_GLB_T"
subTable = "DBX_IDX_SCOPE_LIVE_SUB_T"
ordinaryIndex = "DBX_IDX_SCOPE_LIVE_ORD_I"
localIndex = "DBX_IDX_SCOPE_LIVE_LOC_I"
globalIndex = "DBX_IDX_SCOPE_LIVE_GLB_I"
subIndex = "DBX_IDX_SCOPE_LIVE_SUB_I"
)
tables := []string{ordinaryTable, localTable, globalTable, subTable}
for _, table := range tables {
_ = s.execWithReconnect("DROP TABLE IF EXISTS " + table)
}
defer func() {
for _, table := range tables {
_ = s.execWithReconnect("DROP TABLE IF EXISTS " + table)
}
}()
statements := []string{
"CREATE TABLE " + ordinaryTable + " (ID INTEGER, VALUE VARCHAR(20))",
"CREATE INDEX " + ordinaryIndex + " ON " + ordinaryTable + " (ID)",
"CREATE TABLE " + localTable + " (ID INTEGER, VALUE VARCHAR(20)) PARTITION BY RANGE (ID) PARTITIONS (P1 VALUES LESS THAN (100), P2 VALUES LESS THAN (MAXVALUES))",
"CREATE INDEX " + localIndex + " ON " + localTable + " (VALUE) LOCAL",
"CREATE TABLE " + globalTable + " (ID INTEGER, REGION VARCHAR(20))",
"CREATE INDEX " + globalIndex + " ON " + globalTable + " (ID) GLOBAL PARTITION BY LIST (REGION) PARTITIONS (P_CN VALUES ('CN'), P_US VALUES ('US'), P_OTHER VALUES (OTHERVALUES))",
"CREATE TABLE " + subTable + " (ID INTEGER, REGION VARCHAR(20))",
"CREATE INDEX " + subIndex + " ON " + subTable + " (ID) GLOBAL PARTITION BY RANGE (ID) PARTITIONS (P1 VALUES LESS THAN (100), P2 VALUES LESS THAN (MAXVALUES)) SUBPARTITION BY HASH (REGION) SUBPARTITIONS 2",
}
for _, statement := range statements {
if err := s.execWithReconnect(statement); err != nil {
t.Fatalf("setup statement failed: %s: %v", statement, err)
}
}
tests := []struct {
name string
table string
index string
wantLocal bool
wantGlobal bool
wantSub bool
}{
{name: "ordinary", table: ordinaryTable, index: ordinaryIndex},
{name: "local", table: localTable, index: localIndex, wantLocal: true},
{name: "global", table: globalTable, index: globalIndex, wantGlobal: true},
{name: "global with subpartitions", table: subTable, index: subIndex, wantGlobal: true, wantSub: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Exercise the same reconstructed DDL path used by interactive table
// inspection and by the database-export metadata prefetcher. The
// lower-level index fragment assertion below is not sufficient: a
// regression in table/partition assembly could still omit the index.
fullDDL, err := s.getTableDDL(params.Username, tc.table)
if err != nil {
t.Fatalf("getTableDDL failed: %v", err)
}
if !strings.Contains(strings.ToUpper(fullDDL), "CREATE TABLE") {
t.Fatalf("getTableDDL did not return a CREATE TABLE statement: %s", fullDDL)
}
if !strings.Contains(strings.ToUpper(fullDDL), strings.ToUpper(tc.index)) {
t.Fatalf("getTableDDL omitted index %s: %s", tc.index, fullDDL)
}
t.Logf("full reconstructed table DDL: %s", fullDDL)
indexes, err := s.listIndexes(params.Username, tc.table)
if err != nil {
t.Fatal(err)
}
var got *indexInfo
for i := range indexes {
if strings.EqualFold(indexes[i].Name, tc.index) {
got = &indexes[i]
break
}
}
if got == nil {
t.Fatalf("index %s was not listed: %#v", tc.index, indexes)
}
if got.IsLocal != tc.wantLocal {
t.Fatalf("IsLocal=%v, want %v: %#v", got.IsLocal, tc.wantLocal, *got)
}
if tc.wantGlobal && (got.PartitionType == 0 || !got.PartitionRowsLoaded || len(got.IndexPartitions) == 0) {
t.Fatalf("global partition metadata incomplete: %#v", *got)
}
if tc.wantSub && (got.SubpartitionType == 0 || len(got.IndexSubpartitions) == 0) {
t.Fatalf("subpartition metadata incomplete: %#v", *got)
}
// The table already exists; keep a harmless placeholder statement so
// appendDDLStatement produces the same script shape as a real table
// DDL request without attempting to recreate the table during replay.
ddl := s.appendTableIndexDDL(params.Username, tc.table, "/* existing table DDL */")
t.Logf("reconstructed DDL: %s", ddl)
if tc.wantLocal && !strings.Contains(ddl, " LOCAL") {
t.Fatalf("LOCAL was lost from DDL: %s", ddl)
}
if !tc.wantLocal && !tc.wantGlobal && strings.Contains(ddl, " GLOBAL") {
t.Fatalf("ordinary index was incorrectly labeled GLOBAL: %s", ddl)
}
if tc.wantGlobal && !strings.Contains(ddl, " GLOBAL PARTITION BY ") {
t.Fatalf("GLOBAL partition clause was lost from DDL: %s", ddl)
}
if tc.wantSub && !strings.Contains(ddl, " SUBPARTITION BY HASH ") {
t.Fatalf("GLOBAL subpartition clause was lost from DDL: %s", ddl)
}
// Replay the exact reconstructed index DDL after dropping only this
// isolated test index. This catches syntactically plausible output
// that Xugu nevertheless rejects, especially LOCAL/GLOBAL ordering.
if err := s.execWithReconnect("DROP INDEX IF EXISTS " + tc.table + "." + tc.index); err != nil {
t.Fatalf("drop index before replay: %v", err)
}
if err := s.execWithReconnect(ddl); err != nil {
t.Fatalf("replay reconstructed DDL: %v\nDDL: %s", err, ddl)
}
// Replay the complete table DDL as the database-export path writes it:
// one script containing CREATE TABLE followed by any dependent ALTER
// and CREATE INDEX statements. The agent executes one statement per
// request, so split only the generated statement terminators here.
if err := s.execWithReconnect("DROP TABLE IF EXISTS " + tc.table); err != nil {
t.Fatalf("drop table before full DDL replay: %v", err)
}
for _, statement := range splitGeneratedXuguDDL(fullDDL) {
if err := s.execWithReconnect(statement); err != nil {
t.Fatalf("replay full table DDL: %v\nStatement: %s\nFull DDL: %s", err, statement, fullDDL)
}
}
replayedIndexes, err := s.listIndexes(params.Username, tc.table)
if err != nil {
t.Fatalf("list indexes after full DDL replay: %v", err)
}
if !containsXuguIndex(replayedIndexes, tc.index) {
t.Fatalf("full DDL replay did not recreate index %s: %#v", tc.index, replayedIndexes)
}
})
}
}
func splitGeneratedXuguDDL(ddl string) []string {
var statements []string
for _, part := range strings.Split(ddl, ";") {
part = strings.TrimSpace(part)
if part != "" {
statements = append(statements, part+";")
}
}
return statements
}
func containsXuguIndex(indexes []indexInfo, name string) bool {
for _, index := range indexes {
if strings.EqualFold(index.Name, name) {
return true
}
}
return false
}
+253 -6
View File
@@ -107,6 +107,43 @@ WHERE s.DB_ID = CURRENT_DB_ID
AND s.SCHEMA_NAME = ?
AND t.TABLE_NAME = ?
ORDER BY i.INDEX_NAME`
// Index scope and partition metadata are queried separately from the stable
// index listing query. This keeps ordinary index discovery compatible with
// older Xugu catalog versions while allowing newer versions to preserve
// LOCAL/GLOBAL partition semantics in reconstructed DDL.
const xuguIndexPartitionAttributesSQL = `
SELECT i.INDEX_NAME, i.IS_LOCAL, i.PARTI_TYPE, i.PARTI_NUM, i.PARTI_KEY,
i.SUBPARTI_TYPE, i.SUBPARTI_NUM, i.SUBPARTI_KEY
FROM ALL_INDEXES i
JOIN ALL_TABLES t ON t.DB_ID = i.DB_ID AND t.TABLE_ID = i.TABLE_ID
JOIN ALL_SCHEMAS s ON s.DB_ID = t.DB_ID AND s.SCHEMA_ID = t.SCHEMA_ID
WHERE s.DB_ID = CURRENT_DB_ID
AND s.SCHEMA_NAME = ?
AND t.TABLE_NAME = ?
ORDER BY i.INDEX_NAME`
const xuguIndexPartitionsSQL = `
SELECT i.INDEX_NAME, p.PARTI_NO, p.PARTI_NAME, p.PARTI_VAL
FROM ALL_IDX_PARTIS p
JOIN ALL_INDEXES i ON i.DB_ID = p.DB_ID AND i.INDEX_ID = p.INDEX_ID
JOIN ALL_TABLES t ON t.DB_ID = i.DB_ID AND t.TABLE_ID = i.TABLE_ID
JOIN ALL_SCHEMAS s ON s.DB_ID = t.DB_ID AND s.SCHEMA_ID = t.SCHEMA_ID
WHERE s.DB_ID = CURRENT_DB_ID
AND s.SCHEMA_NAME = ?
AND t.TABLE_NAME = ?
AND i.IS_PRIMARY = FALSE
ORDER BY i.INDEX_NAME, p.PARTI_NO`
const xuguIndexSubpartitionsSQL = `
SELECT i.INDEX_NAME, p.SUBPARTI_NO, p.SUBPARTI_NAME, p.SUBPARTI_VAL
FROM ALL_IDX_SUBPARTIS p
JOIN ALL_INDEXES i ON i.DB_ID = p.DB_ID AND i.INDEX_ID = p.INDEX_ID
JOIN ALL_TABLES t ON t.DB_ID = i.DB_ID AND t.TABLE_ID = i.TABLE_ID
JOIN ALL_SCHEMAS s ON s.DB_ID = t.DB_ID AND s.SCHEMA_ID = t.SCHEMA_ID
WHERE s.DB_ID = CURRENT_DB_ID
AND s.SCHEMA_NAME = ?
AND t.TABLE_NAME = ?
AND i.IS_PRIMARY = FALSE
ORDER BY i.INDEX_NAME, p.SUBPARTI_NO`
const xuguTableMetadataSQL = `
SELECT t.TEMP_TYPE, t.ON_COMMIT_DEL, t.PCTFREE, t.COPY_NUM,
t.PARTI_TYPE, t.PARTI_NUM, t.PARTI_KEY,
@@ -386,7 +423,20 @@ type indexInfo struct {
IndexType *string `json:"index_type"`
IncludedColumns []string `json:"included_columns"`
Comment *string `json:"comment"`
keys []xuguIndexKey
// Partition fields are intentionally internal. The generic DBX index
// protocol does not yet model Xugu-specific index partition clauses, but
// the DDL exporter must retain them to avoid changing index semantics.
IsLocal bool `json:"-"`
PartitionType int `json:"-"`
PartitionCount int `json:"-"`
PartitionKey string `json:"-"`
SubpartitionType int `json:"-"`
SubpartitionCount int `json:"-"`
SubpartitionKey string `json:"-"`
PartitionRowsLoaded bool `json:"-"`
IndexPartitions []xuguPartitionInfo `json:"-"`
IndexSubpartitions []xuguPartitionInfo `json:"-"`
keys []xuguIndexKey
}
func (i indexInfo) MarshalJSON() ([]byte, error) {
@@ -1664,7 +1714,7 @@ func isXuguMetadataAccessError(err error) bool {
catalogObject := false
for _, object := range []string{
"DATABASES", "SCHEMAS", "TABLES", "VIEWS", "COLUMNS", "CONSTRAINTS", "INDEXES",
"TRIGGERS", "PARTIS", "SUBPARTIS", "SEQUENCES", "SYNONYMS", "PROCEDURES", "PACKAGES", "TYPES",
"TRIGGERS", "PARTIS", "SUBPARTIS", "IDX_PARTIS", "IDX_SUBPARTIS", "SEQUENCES", "SYNONYMS", "PROCEDURES", "PACKAGES", "TYPES",
} {
if strings.Contains(message, "ALL_"+object) || strings.Contains(message, "SYS_"+object) {
catalogObject = true
@@ -2648,9 +2698,90 @@ func (s *server) listIndexes(schema, table string) ([]indexInfo, error) {
item.IncludedColumns = []string{}
result = append(result, item)
}
// LOCAL/GLOBAL attributes are best-effort metadata. Keep the stable index
// listing usable when an older Xugu catalog does not expose these columns.
s.loadIndexPartitionMetadata(catalogSchema, catalogTable, result)
return emptyIfNil(result), rows.Err()
}
// loadIndexPartitionMetadata enriches the stable index list with Xugu's
// partition scope and partition definitions. The generic DBX index payload
// does not expose these Xugu-specific fields, so they remain internal and are
// consumed by table DDL reconstruction only.
func (s *server) loadIndexPartitionMetadata(schema, table string, indexes []indexInfo) {
if len(indexes) == 0 {
return
}
byName := make(map[string]*indexInfo, len(indexes))
for i := range indexes {
byName[indexes[i].Name] = &indexes[i]
}
rows, err := s.queryRows(xuguTableCatalogQuery(xuguIndexPartitionAttributesSQL, schema, table), nil)
if err != nil {
return
}
func() {
defer s.closeRows(rows)
for rows.Next() {
var name, local, partitionType, partitionCount, partitionKey any
var subpartitionType, subpartitionCount, subpartitionKey any
if err := rows.Scan(&name, &local, &partitionType, &partitionCount, &partitionKey,
&subpartitionType, &subpartitionCount, &subpartitionKey); err != nil {
return
}
item := byName[xuguString(name)]
if item == nil {
continue
}
item.IsLocal = truthy(local)
item.PartitionType = xuguInt(partitionType)
item.PartitionCount = xuguInt(partitionCount)
item.PartitionKey = xuguString(partitionKey)
item.SubpartitionType = xuguInt(subpartitionType)
item.SubpartitionCount = xuguInt(subpartitionCount)
item.SubpartitionKey = xuguString(subpartitionKey)
}
}()
rows, err = s.queryRows(xuguTableCatalogQuery(xuguIndexPartitionsSQL, schema, table), nil)
if err != nil {
return
}
func() {
defer s.closeRows(rows)
for rows.Next() {
var name, position, partitionName, partitionValue any
if err := rows.Scan(&name, &position, &partitionName, &partitionValue); err != nil {
return
}
if item := byName[xuguString(name)]; item != nil {
item.IndexPartitions = append(item.IndexPartitions, xuguPartitionInfo{
Name: xuguString(partitionName), Value: xuguString(partitionValue),
})
item.PartitionRowsLoaded = true
}
}
}()
rows, err = s.queryRows(xuguTableCatalogQuery(xuguIndexSubpartitionsSQL, schema, table), nil)
if err != nil {
return
}
defer s.closeRows(rows)
for rows.Next() {
var name, position, partitionName, partitionValue any
if err := rows.Scan(&name, &position, &partitionName, &partitionValue); err != nil {
return
}
if item := byName[xuguString(name)]; item != nil {
item.IndexSubpartitions = append(item.IndexSubpartitions, xuguPartitionInfo{
Name: xuguString(partitionName), Value: xuguString(partitionValue),
})
}
}
}
func (s *server) listForeignKeys(schema, table string) ([]foreignKeyInfo, error) {
schema, err := s.normalizeSchema(schema)
if err != nil {
@@ -4555,10 +4686,7 @@ func (s *server) appendTableIndexDDL(schema, table, ddl string) string {
builder.WriteString(renderXuguIndexKey(key))
}
builder.WriteByte(')')
if index.IndexType != nil && strings.TrimSpace(*index.IndexType) != "" {
builder.WriteString(" INDEXTYPE IS ")
builder.WriteString(strings.TrimSpace(*index.IndexType))
}
appendXuguIndexOptions(&builder, index)
builder.WriteByte(';')
}
if builder.Len() == 0 {
@@ -4567,6 +4695,125 @@ func (s *server) appendTableIndexDDL(schema, table, ddl string) string {
return appendDDLStatement(ddl, builder.String())
}
func appendXuguIndexOptions(builder *strings.Builder, index indexInfo) {
if index.IndexType != nil && strings.TrimSpace(*index.IndexType) != "" {
builder.WriteString(" INDEXTYPE IS ")
builder.WriteString(strings.TrimSpace(*index.IndexType))
}
if index.IsLocal {
builder.WriteString(" LOCAL")
return
}
if index.PartitionType == 0 || !index.PartitionRowsLoaded {
return
}
partitionDDL := renderXuguIndexPartitionDDL(index)
if partitionDDL != "" {
builder.WriteString(" GLOBAL")
builder.WriteString(partitionDDL)
}
}
func renderXuguIndexPartitionDDL(index indexInfo) string {
typeName := xuguPartitionType(index.PartitionType)
key := strings.TrimSpace(index.PartitionKey)
if typeName == "" || key == "" {
return ""
}
var builder strings.Builder
builder.WriteString(" PARTITION BY ")
builder.WriteString(typeName)
builder.WriteString(" (")
builder.WriteString(key)
builder.WriteByte(')')
if index.PartitionType == 3 {
if index.PartitionCount <= 0 {
return ""
}
builder.WriteString(fmt.Sprintf(" PARTITIONS %d", index.PartitionCount))
} else if len(index.IndexPartitions) > 0 {
for _, partition := range index.IndexPartitions {
if strings.TrimSpace(partition.Name) == "" || strings.TrimSpace(partition.Value) == "" {
return ""
}
}
builder.WriteString(" PARTITIONS (\n")
for i, partition := range index.IndexPartitions {
if i > 0 {
builder.WriteString(",\n")
}
builder.WriteString(" ")
builder.WriteString(quoteIdentifier(partition.Name))
builder.WriteByte(' ')
if index.PartitionType == 1 {
builder.WriteString("VALUES LESS THAN (")
} else {
builder.WriteString("VALUES (")
}
builder.WriteString(strings.TrimSpace(partition.Value))
builder.WriteByte(')')
}
builder.WriteString("\n)")
} else {
return ""
}
subType := xuguPartitionType(index.SubpartitionType)
subKey := strings.TrimSpace(index.SubpartitionKey)
if subType == "" || subKey == "" {
return builder.String()
}
builder.WriteString(" SUBPARTITION BY ")
builder.WriteString(subType)
builder.WriteString(" (")
builder.WriteString(subKey)
builder.WriteByte(')')
if index.SubpartitionType == 3 {
if index.SubpartitionCount <= 0 {
return builderWithoutSubpartitionDDL(index, builder)
}
builder.WriteString(fmt.Sprintf(" SUBPARTITIONS %d", index.SubpartitionCount))
} else if len(index.IndexSubpartitions) > 0 {
for _, partition := range index.IndexSubpartitions {
if strings.TrimSpace(partition.Name) == "" || strings.TrimSpace(partition.Value) == "" {
return builderWithoutSubpartitionDDL(index, builder)
}
}
builder.WriteString(" SUBPARTITIONS (\n")
for i, partition := range index.IndexSubpartitions {
if i > 0 {
builder.WriteString(",\n")
}
builder.WriteString(" ")
builder.WriteString(quoteIdentifier(partition.Name))
builder.WriteByte(' ')
if index.SubpartitionType == 1 {
builder.WriteString("VALUES LESS THAN (")
} else {
builder.WriteString("VALUES (")
}
builder.WriteString(strings.TrimSpace(partition.Value))
builder.WriteByte(')')
}
builder.WriteString("\n)")
}
return builder.String()
}
// builderWithoutSubpartitionDDL returns the complete first-level definition
// when Xugu exposes a subpartition marker but not enough detail to replay it.
// Emitting an incomplete SUBPARTITION clause is worse than preserving a valid
// global index DDL without that optional detail.
func builderWithoutSubpartitionDDL(index indexInfo, builder strings.Builder) string {
firstLevel := index
firstLevel.SubpartitionType = 0
firstLevel.SubpartitionKey = ""
firstLevel.SubpartitionCount = 0
firstLevel.IndexSubpartitions = nil
return renderXuguIndexPartitionDDL(firstLevel)
}
// uniqueKeyColumnSets returns column lists for PRIMARY KEY and UNIQUE constraints.
// Xugu stores those definitions as quoted identifiers inside ALL_CONSTRAINTS.DEFINE.
func uniqueKeyColumnSets(constraints []xuguConstraintInfo) [][]string {
+195
View File
@@ -964,6 +964,148 @@ func TestIndexSQLUsesLowPrivilegeDictionary(t *testing.T) {
}
}
func TestIndexPartitionMetadataUsesLowPrivilegeDictionary(t *testing.T) {
for name, query := range map[string]string{
"index attributes": xuguIndexPartitionAttributesSQL,
"index partitions": xuguIndexPartitionsSQL,
"index subpartitions": xuguIndexSubpartitionsSQL,
} {
t.Run(name, func(t *testing.T) {
upper := strings.ToUpper(query)
for _, want := range []string{"ALL_INDEXES", "ALL_TABLES", "ALL_SCHEMAS", "CURRENT_DB_ID"} {
if !strings.Contains(upper, want) {
t.Fatalf("%s query should contain %s: %s", name, want, query)
}
}
if name != "index attributes" && !strings.Contains(upper, "ALL_IDX_") {
t.Fatalf("%s query should use the low-privilege index partition view: %s", name, query)
}
for _, forbidden := range []string{"SYS_INDEXES", "SYS_IDX_PARTIS", "SYS_IDX_SUBPARTIS"} {
if strings.Contains(upper, forbidden) {
t.Fatalf("%s query should not use %s: %s", name, forbidden, query)
}
}
})
}
if !strings.Contains(strings.ToUpper(xuguIndexPartitionAttributesSQL), "IS_LOCAL") {
t.Fatal("index attributes query must preserve LOCAL scope")
}
for _, query := range []string{xuguIndexPartitionAttributesSQL, xuguIndexPartitionsSQL, xuguIndexSubpartitionsSQL} {
upper := strings.ToUpper(query)
if !strings.Contains(upper, "SCHEMA_NAME = ?") || !strings.Contains(upper, "TABLE_NAME = ?") {
t.Fatalf("index metadata query must be scoped to the resolved schema/table: %s", query)
}
}
}
func TestXuguIndexScopeDDL(t *testing.T) {
indexType := "BTREE"
cases := []struct {
name string
index indexInfo
want string
}{
{
name: "ordinary index does not invent GLOBAL",
index: indexInfo{IndexType: &indexType},
want: " INDEXTYPE IS BTREE",
},
{
name: "local partition index",
index: indexInfo{IndexType: &indexType, IsLocal: true},
want: " INDEXTYPE IS BTREE LOCAL",
},
{
name: "global range partition index",
index: indexInfo{
IndexType: indexTypePtr("BTREE"), PartitionType: 1, PartitionKey: `"CREATED_AT"`,
PartitionRowsLoaded: true,
IndexPartitions: []xuguPartitionInfo{
{Name: "P1", Value: "'2025-01-01'"},
{Name: "P2", Value: "'2026-01-01'"},
},
},
want: " GLOBAL PARTITION BY RANGE (\"CREATED_AT\") PARTITIONS (",
},
{
name: "global hash partition index",
index: indexInfo{
IndexType: indexTypePtr("BTREE"), PartitionType: 3, PartitionCount: 4,
PartitionKey: `"CUSTOMER_ID"`, PartitionRowsLoaded: true,
},
want: " GLOBAL PARTITION BY HASH (\"CUSTOMER_ID\") PARTITIONS 4",
},
{
name: "incomplete global metadata is not emitted",
index: indexInfo{IndexType: indexTypePtr("BTREE"), PartitionType: 1, PartitionKey: `"ID"`},
want: " INDEXTYPE IS BTREE",
},
{
name: "global hash without a count is not emitted",
index: indexInfo{IndexType: indexTypePtr("BTREE"), PartitionType: 3, PartitionKey: `"ID"`, PartitionRowsLoaded: true},
want: " INDEXTYPE IS BTREE",
},
{
name: "malformed global partition row is not emitted",
index: indexInfo{
IndexType: indexTypePtr("BTREE"), PartitionType: 2, PartitionKey: `"REGION"`,
PartitionRowsLoaded: true, IndexPartitions: []xuguPartitionInfo{{Name: "P1"}},
},
want: " INDEXTYPE IS BTREE",
},
{
name: "incomplete subpartition keeps valid first level",
index: indexInfo{
IndexType: indexTypePtr("BTREE"), PartitionType: 2, PartitionKey: `"REGION"`,
PartitionRowsLoaded: true, IndexPartitions: []xuguPartitionInfo{{Name: "P1", Value: "'CN'"}},
SubpartitionType: 3, SubpartitionKey: `"ID"`,
},
want: " GLOBAL PARTITION BY LIST (\"REGION\") PARTITIONS (",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var builder strings.Builder
appendXuguIndexOptions(&builder, tc.index)
got := builder.String()
if got != tc.want && !strings.Contains(got, tc.want) {
t.Fatalf("index option DDL = %q, want %q", got, tc.want)
}
if tc.name == "ordinary index does not invent GLOBAL" && strings.Contains(got, "GLOBAL") {
t.Fatalf("ordinary index must not be labeled GLOBAL: %q", got)
}
if tc.name == "incomplete subpartition keeps valid first level" && strings.Contains(got, "SUBPARTITION") {
t.Fatalf("incomplete subpartition metadata must not produce a partial clause: %q", got)
}
})
}
}
func indexTypePtr(value string) *string { return &value }
func TestXuguIndexPartitionDetailsStayInternalToTheGenericPayload(t *testing.T) {
data, err := json.Marshal(indexInfo{
Name: "IDX_LOCAL", Columns: []string{"ID"}, IsLocal: true,
PartitionType: 1, PartitionKey: `"ID"`, PartitionRowsLoaded: true,
IndexPartitions: []xuguPartitionInfo{{Name: "P1", Value: "MAXVALUES"}},
})
if err != nil {
t.Fatal(err)
}
text := string(data)
for _, forbidden := range []string{"is_local", "partition_type", "partition_key", "index_partitions"} {
if strings.Contains(text, forbidden) {
t.Fatalf("Xugu-specific index partition field %q leaked into generic metadata: %s", forbidden, text)
}
}
for _, required := range []string{`"name":"IDX_LOCAL"`, `"columns":["ID"]`} {
if !strings.Contains(text, required) {
t.Fatalf("generic index metadata lost %q: %s", required, text)
}
}
}
func TestTableChildMetadataUsesLowPrivilegeDictionary(t *testing.T) {
for name, query := range map[string]string{
"constraints": xuguTableConstraintsSQL,
@@ -2448,6 +2590,7 @@ func init() {
sql.Register("xugu-test-eof", &xuguEOFDriver{})
sql.Register("xugu-test-trigger-details", &xuguTriggerDetailsDriver{})
sql.Register("xugu-test-schema-listing", &xuguSchemaListingDriver{})
sql.Register("xugu-test-index-partition-fallback", &xuguIndexPartitionFallbackDriver{})
}
var xuguSchemaListingState struct {
@@ -2604,6 +2747,24 @@ func TestMetadataPermissionFallbackDoesNotReturnRPCError(t *testing.T) {
}
}
func TestIndexListingSurvivesUnavailablePartitionCatalog(t *testing.T) {
db, err := sql.Open("xugu-test-index-partition-fallback", "")
if err != nil {
t.Fatal(err)
}
defer db.Close()
s := newServer()
s.db = db
indexes, err := s.listIndexes("APP", "T")
if err != nil {
t.Fatal(err)
}
if len(indexes) != 1 || indexes[0].Name != "IDX_T" || indexes[0].IsLocal {
t.Fatalf("stable index listing should survive unavailable partition metadata: %#v", indexes)
}
}
func TestGetColumnsFallsBackToDirectObjectAccessOnMetadataPermission(t *testing.T) {
db, err := sql.Open("xugu-test-permission-metadata", "")
if err != nil {
@@ -2722,6 +2883,40 @@ type xuguShowResultDriver struct{}
type xuguPermissionMetadataDriver struct{}
type xuguIndexPartitionFallbackDriver struct{}
func (d *xuguIndexPartitionFallbackDriver) Open(string) (driver.Conn, error) {
return &xuguIndexPartitionFallbackConn{}, nil
}
type xuguIndexPartitionFallbackConn struct{}
func (c *xuguIndexPartitionFallbackConn) Prepare(string) (driver.Stmt, error) {
return nil, errors.New("not supported")
}
func (c *xuguIndexPartitionFallbackConn) Close() error { return nil }
func (c *xuguIndexPartitionFallbackConn) Begin() (driver.Tx, error) {
return nil, errors.New("not supported")
}
func (c *xuguIndexPartitionFallbackConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
upper := strings.ToUpper(query)
switch {
case strings.Contains(upper, "SELECT S.SCHEMA_NAME, T.TABLE_NAME"):
return &xuguStaticRows{columns: []string{"SCHEMA_NAME", "TABLE_NAME"}, values: [][]driver.Value{{"APP", "T"}}}, nil
case strings.Contains(upper, "I.IS_LOCAL"):
return nil, errors.New("unknown column IS_LOCAL in older Xugu catalog")
case strings.Contains(upper, "FROM ALL_IDX_PARTIS"), strings.Contains(upper, "FROM ALL_IDX_SUBPARTIS"):
return nil, errors.New("index partition views are unavailable in older Xugu catalog")
case strings.Contains(upper, "SELECT I.INDEX_NAME, I.KEYS"):
return &xuguStaticRows{
columns: []string{"INDEX_NAME", "KEYS", "IS_UNIQUE", "IS_PRIMARY", "INDEX_TYPE", "FILTER"},
values: [][]driver.Value{{"IDX_T", `"ID"`, false, false, int64(0), nil}},
}, nil
default:
return nil, fmt.Errorf("unexpected index fallback query: %s", query)
}
}
type xuguFallbackErrorDriver struct{}
func (d *xuguFallbackErrorDriver) Open(name string) (driver.Conn, error) {