fix(xugu): expose user-managed sequence DDL

This commit is contained in:
Elias 2026-07-28 22:45:03 +08:00 committed by GitHub
parent 5f2fd528b0
commit 99cfa1b516
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 354 additions and 0 deletions

View File

@ -40,6 +40,11 @@ const xuguCatalogTableNameSelectSQL = `
SELECT s.SCHEMA_NAME, t.TABLE_NAME
FROM ALL_TABLES t
JOIN ALL_SCHEMAS s ON s.DB_ID = t.DB_ID AND s.SCHEMA_ID = t.SCHEMA_ID`
const xuguCatalogSequenceNameSelectSQL = `
SELECT s.SCHEMA_NAME, q.SEQ_NAME
FROM ALL_SEQUENCES q
JOIN ALL_SCHEMAS s ON s.DB_ID = q.DB_ID AND s.SCHEMA_ID = q.SCHEMA_ID
WHERE q.IS_SYS = FALSE`
const xuguPrimaryKeyColumnsSQL = `
SELECT c.DEFINE
FROM ALL_CONSTRAINTS c
@ -411,6 +416,21 @@ type xuguIdentityInfo struct {
SystemGenerated bool
}
// xuguSequenceMetadata contains the ALL_SEQUENCES fields needed to reproduce
// a user-managed sequence. IS_ORDER and VALID are runtime/catalog state and
// do not have CREATE SEQUENCE clauses, so they are intentionally omitted.
type xuguSequenceMetadata struct {
Schema string
Name string
Current any
Minimum any
Maximum any
Step any
Cache any
Cycle any
Comment any
}
// xuguIndexKey preserves the catalog spelling and SQL semantics of an index
// key. In particular, an index key can be a normal identifier, an identifier
// with ASC/DESC ordering, or an arbitrary expression such as LOWER("CODE").
@ -1617,6 +1637,7 @@ SELECT q.SEQ_NAME AS OBJECT_NAME, 'SEQUENCE' AS OBJECT_TYPE, NULL AS COMMENTS, N
FROM ALL_SEQUENCES q
JOIN ALL_SCHEMAS s ON s.DB_ID = q.DB_ID AND s.SCHEMA_ID = q.SCHEMA_ID
WHERE UPPER(s.SCHEMA_NAME) = UPPER(?)
AND q.IS_SYS = FALSE
UNION ALL
SELECT u.TYPE_NAME AS OBJECT_NAME, 'TYPE' AS OBJECT_TYPE, u.COMMENTS, u.VALID
FROM ALL_TYPES u
@ -2002,6 +2023,9 @@ func (s *server) listSubpartitions(schema, table string) ([]subpartitionInfo, er
}
func (s *server) getObjectSource(schema, name, objectType string) (map[string]any, error) {
if strings.EqualFold(strings.TrimSpace(objectType), "SEQUENCE") {
return s.getSequenceSource(schema, name)
}
var err error
schema, err = s.normalizeSchema(schema)
if err != nil {
@ -2033,6 +2057,186 @@ func (s *server) getObjectSource(schema, name, objectType string) (map[string]an
return result, rows.Err()
}
// getSequenceSource reconstructs sequence DDL from ALL_SEQUENCES. Unlike
// programmable objects, sequences have no stored DEFINE text. Reconstructing
// the statement avoids DBMS_METADATA permissions and matches the approach used
// by the Xugu DBeaver extension.
func (s *server) getSequenceSource(schema, name string) (map[string]any, error) {
catalogSchema, catalogName, err := s.resolveCatalogSequenceName(schema, name)
if err != nil {
return nil, err
}
rows, err := s.queryRows(xuguSequenceMetadataQuery(catalogSchema, catalogName), nil)
if err != nil {
return nil, err
}
defer s.closeRows(rows)
result := map[string]any{"name": catalogName, "object_type": "SEQUENCE", "schema": catalogSchema, "source": "", "editable": false}
if !rows.Next() {
return result, rows.Err()
}
var sequence xuguSequenceMetadata
if err := rows.Scan(
&sequence.Schema, &sequence.Name,
&sequence.Current, &sequence.Minimum, &sequence.Maximum, &sequence.Step,
&sequence.Cache, &sequence.Cycle, &sequence.Comment,
); err != nil {
return nil, err
}
result["name"] = sequence.Name
result["schema"] = sequence.Schema
result["source"] = renderXuguSequenceDDL(sequence)
return result, rows.Err()
}
// resolveCatalogSequenceName follows the same exact-case-first policy used by
// table DDL export. A case-insensitive lookup is only safe when it resolves to
// one catalog object; selecting the first row would export a different quoted
// sequence when catalog objects differ only by case.
func (s *server) resolveCatalogSequenceName(schema, name string) (string, string, error) {
schema = strings.TrimSpace(schema)
name = strings.TrimSpace(name)
if schema == "" {
current, err := s.currentSchema()
if err != nil {
return "", "", err
}
schema = current
}
if name == "" {
return "", "", errors.New("sequence name is required")
}
candidates, err := s.catalogSequenceNameCandidates(xuguCatalogSequenceNameQuery(schema, name, false))
if err != nil {
return "", "", err
}
if len(candidates) > 0 {
return selectXuguCatalogSequenceName(schema, name, candidates)
}
candidates, err = s.catalogSequenceNameCandidates(xuguCatalogSequenceNameQuery(schema, name, true))
if err != nil {
return "", "", err
}
return selectXuguCatalogSequenceName(schema, name, candidates)
}
func xuguSequenceMetadataQuery(schema, name string) string {
return `
SELECT s.SCHEMA_NAME, q.SEQ_NAME,
q.CURR_VAL, q.MIN_VAL, q.MAX_VAL, q.STEP_VAL,
q.CACHE_VAL, q.IS_CYCLE, q.COMMENTS
FROM ALL_SEQUENCES q
JOIN ALL_SCHEMAS s ON s.DB_ID = q.DB_ID AND s.SCHEMA_ID = q.SCHEMA_ID
WHERE s.SCHEMA_NAME = ` + quoteStringLiteral(schema) + `
AND q.SEQ_NAME = ` + quoteStringLiteral(name) + `
AND q.IS_SYS = FALSE`
}
type xuguCatalogSequenceName struct {
Schema string
Name string
}
func xuguCatalogSequenceNameQuery(schema, name string, caseInsensitive bool) string {
schemaExpr := quoteStringLiteral(schema)
nameExpr := quoteStringLiteral(name)
if caseInsensitive {
schemaExpr = quoteStringLiteral(strings.ToUpper(schema))
nameExpr = quoteStringLiteral(strings.ToUpper(name))
return xuguCatalogSequenceNameSelectSQL + "\n AND UPPER(s.SCHEMA_NAME) = " + schemaExpr +
"\n AND UPPER(q.SEQ_NAME) = " + nameExpr
}
return xuguCatalogSequenceNameSelectSQL + "\n AND s.SCHEMA_NAME = " + schemaExpr +
"\n AND q.SEQ_NAME = " + nameExpr
}
func (s *server) catalogSequenceNameCandidates(query string) ([]xuguCatalogSequenceName, error) {
rows, err := s.queryRows(strings.TrimSpace(query), nil)
if err != nil {
return nil, err
}
defer s.closeRows(rows)
var candidates []xuguCatalogSequenceName
for rows.Next() {
var candidate xuguCatalogSequenceName
if err := rows.Scan(&candidate.Schema, &candidate.Name); err != nil {
return nil, err
}
candidates = append(candidates, candidate)
}
if err := rows.Err(); err != nil {
return nil, err
}
return candidates, nil
}
func selectXuguCatalogSequenceName(schema, name string, candidates []xuguCatalogSequenceName) (string, string, error) {
for _, candidate := range candidates {
if candidate.Schema == schema && candidate.Name == name {
return candidate.Schema, candidate.Name, nil
}
}
if len(candidates) == 1 {
return candidates[0].Schema, candidates[0].Name, nil
}
if len(candidates) == 0 {
return "", "", fmt.Errorf("sequence not found: %s.%s", schema, name)
}
return "", "", fmt.Errorf("sequence name is ambiguous: %s.%s; specify the catalog's exact case", schema, name)
}
func renderXuguSequenceDDL(sequence xuguSequenceMetadata) string {
var builder strings.Builder
builder.WriteString("CREATE SEQUENCE ")
builder.WriteString(quoteIdentifier(sequence.Schema))
builder.WriteByte('.')
builder.WriteString(quoteIdentifier(sequence.Name))
if value := xuguSequenceNumber(sequence.Step); value != "" {
builder.WriteString("\n INCREMENT BY ")
builder.WriteString(value)
}
if value := xuguSequenceNumber(sequence.Current); value != "" {
builder.WriteString("\n START WITH ")
builder.WriteString(value)
}
if value := xuguSequenceNumber(sequence.Minimum); value != "" {
builder.WriteString("\n MINVALUE ")
builder.WriteString(value)
} else {
builder.WriteString("\n NOMINVALUE")
}
if value := xuguSequenceNumber(sequence.Maximum); value != "" {
builder.WriteString("\n MAXVALUE ")
builder.WriteString(value)
} else {
builder.WriteString("\n NOMAXVALUE")
}
if value := xuguSequenceNumber(sequence.Cache); value != "" && xuguInt(sequence.Cache) > 1 {
builder.WriteString("\n CACHE ")
builder.WriteString(value)
} else {
builder.WriteString("\n NOCACHE")
}
if truthy(sequence.Cycle) {
builder.WriteString("\n CYCLE")
} else {
builder.WriteString("\n NOCYCLE")
}
if comment := strings.TrimSpace(xuguString(sequence.Comment)); comment != "" {
builder.WriteString("\n COMMENT ")
builder.WriteString(quoteStringLiteral(comment))
}
builder.WriteString(";")
return builder.String()
}
func xuguSequenceNumber(value any) string {
return strings.TrimSpace(xuguString(value))
}
func (s *server) getTableDDL(schema, table string) (string, error) {
// Resolve the catalog's stored casing before issuing exact metadata lookups,
// so emitted DDL quotes the original names and preserves double-quoted

View File

@ -775,6 +775,71 @@ func TestXuguListObjectsQueryIncludesProgrammableObjects(t *testing.T) {
assertArgs(t, query.Args, wantArgs)
}
func TestXuguListObjectsQueryExcludesSystemSequences(t *testing.T) {
query := xuguListObjectsQuery("APP", metadataListConstraints{
ObjectTypes: []string{"sequence"},
})
if !strings.Contains(query.SQL, "q.IS_SYS = FALSE") {
t.Fatalf("sequence lookup must exclude system-managed identity sequences:\n%s", query.SQL)
}
}
func TestGetSequenceSourceReconstructsExecutableDDL(t *testing.T) {
db, err := sql.Open("xugu-test-sequence-source", "")
if err != nil {
t.Fatal(err)
}
defer db.Close()
s := newServer()
s.db = db
source, err := s.getObjectSource("AppSchema", "seqOrderNo", "SEQUENCE")
if err != nil {
t.Fatalf("get sequence source: %v", err)
}
if source["editable"] != false {
t.Fatalf("sequence source must remain read-only: %#v", source)
}
if source["schema"] != "AppSchema" || source["name"] != "seqOrderNo" {
t.Fatalf("sequence source must preserve catalog spelling: %#v", source)
}
ddl, _ := source["source"].(string)
for _, want := range []string{
`CREATE SEQUENCE "AppSchema"."seqOrderNo"`,
"INCREMENT BY 10",
"START WITH 500",
"MINVALUE -100",
"MAXVALUE 10000",
"CACHE 20",
"CYCLE",
"COMMENT 'order''s next number'",
} {
if !strings.Contains(ddl, want) {
t.Fatalf("sequence DDL is missing %q:\n%s", want, ddl)
}
}
if !strings.HasSuffix(strings.TrimSpace(ddl), ";") {
t.Fatalf("sequence DDL must end with a statement terminator:\n%s", ddl)
}
}
func TestRenderXuguSequenceDDLUsesNoCacheAndNoCycle(t *testing.T) {
ddl := renderXuguSequenceDDL(xuguSequenceMetadata{
Schema: "APP", Name: "SEQ_DEFAULTS", Current: int64(1), Minimum: int64(1),
Maximum: int64(9223372036854775807), Step: int64(1), Cache: int64(1), Cycle: false,
})
for _, want := range []string{"NOCACHE", "NOCYCLE"} {
if !strings.Contains(ddl, want) {
t.Fatalf("sequence DDL is missing %q:\n%s", want, ddl)
}
}
if strings.Contains(ddl, "NO CYCLE") {
t.Fatalf("sequence DDL must use Xugu's NOCYCLE spelling:\n%s", ddl)
}
}
func TestXuguObjectSourceQuerySupportsSharedObjectKinds(t *testing.T) {
for _, objectType := range []string{"TRIGGER", "PACKAGE_BODY", "TYPE", "TYPE_BODY"} {
query, _, err := objectSourceQuery("APP", "demo", objectType)
@ -1313,6 +1378,27 @@ func TestSelectXuguCatalogTableNamePrefersExactCaseAndRejectsAmbiguity(t *testin
}
}
func TestSelectXuguCatalogSequenceNamePrefersExactCaseAndRejectsAmbiguity(t *testing.T) {
candidates := []xuguCatalogSequenceName{
{Schema: "AppSchema", Name: "seqOrderNo"},
{Schema: "AppSchema", Name: "SEQORDERNO"},
}
schema, name, err := selectXuguCatalogSequenceName("AppSchema", "seqOrderNo", candidates)
if err != nil || schema != "AppSchema" || name != "seqOrderNo" {
t.Fatalf("exact-case selection = (%q, %q, %v), want quoted catalog sequence", schema, name, err)
}
if _, _, err := selectXuguCatalogSequenceName("AppSchema", "SeqOrderNo", candidates); err == nil || !strings.Contains(err.Error(), "ambiguous") {
t.Fatalf("mixed-case ambiguous selection error = %v, want ambiguity error", err)
}
schema, name, err = selectXuguCatalogSequenceName("appschema", "seq_plain", []xuguCatalogSequenceName{{Schema: "APPSCHEMA", Name: "SEQ_PLAIN"}})
if err != nil || schema != "APPSCHEMA" || name != "SEQ_PLAIN" {
t.Fatalf("single-candidate fallback = (%q, %q, %v), want catalog spelling", schema, name, err)
}
}
func TestCatalogTableLookupQueriesAvoidCaseFoldingBoundParameters(t *testing.T) {
exact := xuguCatalogTableNameQuery("S'CHEMA", "MiX'ed", false)
if strings.Contains(strings.ToUpper(exact), "UPPER(") {
@ -1333,6 +1419,28 @@ func TestCatalogTableLookupQueriesAvoidCaseFoldingBoundParameters(t *testing.T)
}
}
func TestCatalogSequenceLookupQueriesPreferExactIdentifiers(t *testing.T) {
exact := xuguCatalogSequenceNameQuery("App'Schema", "seq'MixedCase", false)
if strings.Contains(strings.ToUpper(exact), "UPPER(") {
t.Fatalf("exact sequence lookup must not case-fold identifiers:\n%s", exact)
}
for _, fragment := range []string{"q.IS_SYS = FALSE", "s.SCHEMA_NAME = 'App''Schema'", "q.SEQ_NAME = 'seq''MixedCase'"} {
if !strings.Contains(exact, fragment) {
t.Fatalf("exact sequence lookup missing %q:\n%s", fragment, exact)
}
}
folded := xuguCatalogSequenceNameQuery("App'Schema", "seq'MixedCase", true)
if strings.Contains(folded, "UPPER(?)") {
t.Fatalf("case-insensitive sequence lookup must not call UPPER(?) on bound parameters:\n%s", folded)
}
for _, fragment := range []string{"q.IS_SYS = FALSE", "UPPER(s.SCHEMA_NAME) = 'APP''SCHEMA'", "UPPER(q.SEQ_NAME) = 'SEQ''MIXEDCASE'"} {
if !strings.Contains(folded, fragment) {
t.Fatalf("case-insensitive sequence lookup missing %q:\n%s", fragment, folded)
}
}
}
func TestTableDDLCatalogQueriesUseExactIdentifiers(t *testing.T) {
queries := map[string]string{
"primary key": xuguPrimaryKeyColumnsSQL,
@ -1613,6 +1721,7 @@ func init() {
sql.Register("xugu-test-table-objects", &xuguTableObjectsDriver{})
sql.Register("xugu-test-table-ddl", &xuguTableDDLDriver{})
sql.Register("xugu-test-show-result", &xuguShowResultDriver{})
sql.Register("xugu-test-sequence-source", &xuguSequenceSourceDriver{})
}
type xuguShowResultDriver struct{}
@ -1671,6 +1780,47 @@ func (c *xuguShowResultConn) ExecContext(_ context.Context, query string, _ []dr
return nil, fmt.Errorf("SHOW statement was incorrectly sent to ExecContext: %s", query)
}
type xuguSequenceSourceDriver struct{}
func (d *xuguSequenceSourceDriver) Open(name string) (driver.Conn, error) {
return &xuguSequenceSourceConn{}, nil
}
type xuguSequenceSourceConn struct{}
func (c *xuguSequenceSourceConn) Prepare(query string) (driver.Stmt, error) {
return nil, errors.New("not supported")
}
func (c *xuguSequenceSourceConn) Close() error { return nil }
func (c *xuguSequenceSourceConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
func (c *xuguSequenceSourceConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
upper := strings.ToUpper(query)
if !strings.Contains(upper, "FROM ALL_SEQUENCES") || !strings.Contains(upper, "Q.IS_SYS = FALSE") {
return nil, fmt.Errorf("unexpected sequence source query: %s", query)
}
if strings.Contains(upper, "Q.CURR_VAL") {
if strings.Contains(upper, "UPPER(") || !strings.Contains(query, "s.SCHEMA_NAME = 'AppSchema'") || !strings.Contains(query, "q.SEQ_NAME = 'seqOrderNo'") {
return nil, fmt.Errorf("sequence metadata must use exact catalog identifiers: %s", query)
}
return &xuguStaticRows{
columns: []string{"SCHEMA_NAME", "SEQ_NAME", "CURR_VAL", "MIN_VAL", "MAX_VAL", "STEP_VAL", "CACHE_VAL", "IS_CYCLE", "COMMENTS"},
values: [][]driver.Value{{"AppSchema", "seqOrderNo", int64(500), int64(-100), int64(10000), int64(10), int64(20), true, "order's next number"}},
}, nil
}
if strings.Contains(upper, "SELECT S.SCHEMA_NAME, Q.SEQ_NAME") {
if strings.Contains(upper, "UPPER(") || !strings.Contains(query, "s.SCHEMA_NAME = 'AppSchema'") || !strings.Contains(query, "q.SEQ_NAME = 'seqOrderNo'") {
return nil, fmt.Errorf("sequence resolution must prioritize exact catalog identifiers: %s", query)
}
return &xuguStaticRows{
columns: []string{"SCHEMA_NAME", "SEQ_NAME"},
values: [][]driver.Value{{"AppSchema", "seqOrderNo"}},
}, nil
}
return &xuguStaticRows{
columns: []string{"SCHEMA_NAME", "SEQ_NAME"},
}, nil
}
type xuguTableDDLDriver struct{}
func (d *xuguTableDDLDriver) Open(name string) (driver.Conn, error) {