fix(xugu): expose user-managed sequence DDL
This commit is contained in:
parent
5f2fd528b0
commit
99cfa1b516
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue