fix(oracle): include dictionary comments in table and view DDL

This commit is contained in:
Mo-Moore authored and GitHub committed 2026-09-17 19:28:50 +08:00
1 parent 84019b4271
commit e00fbabb35
4 files changed
+791 -18

No files matched your search

+56 -2
View File
@@ -3389,10 +3389,64 @@ func (s *server) buildViewDDL(schema, name string) (string, error) {
}
trimmed := strings.TrimSpace(source)
upperSource := strings.ToUpper(trimmed)
var ddl string
if strings.HasPrefix(upperSource, "CREATE ") || strings.HasPrefix(upperSource, "ALTER ") {
return trimmed, nil
ddl = trimmed
} else {
ddl = fmt.Sprintf("CREATE OR REPLACE VIEW %s.%s AS\n%s", quoteIdentifier(schema), quoteIdentifier(name), trimmed)
}
return fmt.Sprintf("CREATE OR REPLACE VIEW %s.%s AS\n%s", quoteIdentifier(schema), quoteIdentifier(name), trimmed), nil
return s.appendViewCommentDDLs(schema, name, ddl), nil
}
// appendViewCommentDDLs appends COMMENT ON TABLE/COLUMN statements for views.
// Oracle stores view comments in ALL_TAB_COMMENTS / ALL_COL_COMMENTS the same way as tables.
func (s *server) appendViewCommentDDLs(schema, name, viewDDL string) string {
comments, err := s.loadTableCommentDDLs(schema, name)
if err != nil || len(comments) == 0 {
return viewDDL
}
var builder strings.Builder
builder.WriteString(terminateOracleViewDDL(strings.TrimSpace(viewDDL)))
for _, comment := range comments {
appendOracleDDLFragment(&builder, comment)
}
return builder.String()
}
func terminateOracleViewDDL(ddl string) string {
var lastCode byte
trailingLineComment := false
for pos := 0; pos < len(ddl); pos++ {
if isSQLWhitespace(ddl[pos]) {
continue
}
if ddl[pos] == '-' && pos+1 < len(ddl) && ddl[pos+1] == '-' {
pos = skipLineCommentSQL(ddl, pos)
trailingLineComment = true
continue
}
if ddl[pos] == '/' && pos+1 < len(ddl) && ddl[pos+1] == '*' {
pos = skipBlockCommentSQL(ddl, pos)
trailingLineComment = false
continue
}
if end, ok := skipOracleAlternativeQuotedSQL(ddl, pos); ok {
pos = end
} else if ddl[pos] == '\'' {
pos = skipSingleQuotedSQL(ddl, pos)
} else if ddl[pos] == '"' {
pos = skipDoubleQuotedSQL(ddl, pos)
}
lastCode = ddl[pos]
trailingLineComment = false
}
if lastCode == ';' || lastCode == '/' {
return ddl
}
if trailingLineComment {
return ddl + "\n;"
}
return ddl + ";"
}
func (s *server) getViewSource(schema, name string) (string, error) {
+183
View File
@@ -629,6 +629,189 @@ func TestGetTableDDLAppendsIndexesTriggersAndComments(t *testing.T) {
}
}
func TestBuildViewDDLAppendsComments(t *testing.T) {
const schema = "HR"
const view = "ACTIVE_ORDERS"
const viewText = `SELECT "ID", "STATUS" FROM "HR"."ORDERS" WHERE "STATUS" = 'OPEN'`
db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{
{
queryContains: "FROM ALL_VIEWS",
args: []driver.Value{schema, view},
rows: [][]driver.Value{{viewText}},
},
{
queryContains: "FROM ALL_TAB_COMMENTS",
args: []driver.Value{schema, view},
rows: [][]driver.Value{{"Open orders view"}},
},
{
queryContains: "FROM ALL_COL_COMMENTS",
args: []driver.Value{schema, view},
columns: []string{"COLUMN_NAME", "COMMENTS"},
rows: [][]driver.Value{{"STATUS", "Order status"}},
},
})
s := newServer()
s.db = db
got, err := s.buildViewDDL(schema, view)
if err != nil {
t.Fatal(err)
}
for _, fragment := range []string{
`CREATE OR REPLACE VIEW "HR"."ACTIVE_ORDERS" AS`,
viewText,
`COMMENT ON TABLE "HR"."ACTIVE_ORDERS" IS 'Open orders view';`,
`COMMENT ON COLUMN "HR"."ACTIVE_ORDERS"."STATUS" IS 'Order status';`,
} {
if !strings.Contains(got, fragment) {
t.Fatalf("buildViewDDL() missing %q:\n%s", fragment, got)
}
}
if !strings.Contains(got, viewText+";\n\nCOMMENT ON TABLE") {
t.Fatalf("view DDL should be terminated before comment DDL:\n%s", got)
}
if scripted.next != len(scripted.steps) {
t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next)
}
}
func TestBuildViewDDLCommentBoundaries(t *testing.T) {
const query = `SELECT 1 AS "ID" FROM DUAL`
for _, test := range []struct {
name string
source string
terminated string
noComments bool
tableError bool
columnError bool
columnOnly bool
tableOnly bool
metadataFallback bool
}{
{name: "plain", source: query, terminated: query + ";"},
{name: "line comment", source: query + " -- trailing", terminated: query + " -- trailing\n;"},
{name: "semicolon in comment", source: query + " -- trailing;", terminated: query + " -- trailing;\n;"},
{name: "slash in comment", source: query + " -- trailing /", terminated: query + " -- trailing /\n;"},
{name: "existing terminator", source: query + ";", terminated: query + ";"},
{name: "slash delimiter", source: query + "\n/", terminated: query + "\n/"},
{name: "terminated before line comment", source: query + "; -- trailing;", terminated: query + "; -- trailing;"},
{name: "terminated before block comment", source: query + "; /* trailing; */", terminated: query + "; /* trailing; */"},
{name: "block comment", source: query + " /* trailing; */", terminated: query + " /* trailing; */;"},
{name: "quoted comment markers", source: `SELECT '--;', q'[owner's --;]' AS "--ID" FROM DUAL -- tail`, terminated: `SELECT '--;', q'[owner's --;]' AS "--ID" FROM DUAL -- tail` + "\n;"},
{name: "full create", source: `CREATE VIEW "HR"."ACTIVE_ORDERS" AS ` + query + " -- tail", terminated: `CREATE VIEW "HR"."ACTIVE_ORDERS" AS ` + query + " -- tail\n;"},
{name: "metadata fallback", source: `CREATE VIEW "HR"."ACTIVE_ORDERS" AS ` + query + " -- tail;", terminated: `CREATE VIEW "HR"."ACTIVE_ORDERS" AS ` + query + " -- tail;\n;", metadataFallback: true},
{name: "column only", source: query + " -- tail;", terminated: query + " -- tail;\n;", columnOnly: true},
{name: "table only", source: query + " -- tail;", terminated: query + " -- tail;\n;", tableOnly: true},
{name: "no comments", source: query, noComments: true},
{name: "no comments with trailing comment", source: query + " -- tail;\n", noComments: true},
{name: "table lookup failure", source: query + " -- tail", tableError: true},
{name: "column lookup failure", source: query + " -- tail", columnError: true},
} {
t.Run(test.name, func(t *testing.T) {
tableStep := oracleViewSourceQueryStep{queryContains: "FROM ALL_TAB_COMMENTS", args: []driver.Value{"HR", "ACTIVE_ORDERS"}}
columnStep := oracleViewSourceQueryStep{queryContains: "FROM ALL_COL_COMMENTS", args: []driver.Value{"HR", "ACTIVE_ORDERS"}, columns: []string{"COLUMN_NAME", "COMMENTS"}}
if !test.noComments {
if !test.columnOnly {
tableStep.rows = [][]driver.Value{{"View's comment"}}
}
if !test.tableOnly {
columnStep.rows = [][]driver.Value{{"ID", "Column's comment"}}
}
}
if test.tableError {
tableStep.err = errors.New("dictionary denied")
}
if test.columnError {
columnStep.err = errors.New("dictionary denied")
}
steps := []oracleViewSourceQueryStep{
{queryContains: "FROM ALL_VIEWS", args: []driver.Value{"HR", "ACTIVE_ORDERS"}, rows: [][]driver.Value{{test.source}}},
}
if test.metadataFallback {
steps[0].err = errors.New("view text unavailable")
steps = append(steps, oracleViewSourceQueryStep{
queryContains: "DBMS_METADATA.GET_DDL('VIEW'", args: []driver.Value{"ACTIVE_ORDERS", "HR"}, rows: [][]driver.Value{{test.source}},
})
}
steps = append(steps, tableStep)
if !test.tableError {
steps = append(steps, columnStep)
}
database, scripted := openOracleViewSourceTestDB(t, steps)
server := newServer()
server.db = database
got, err := server.buildViewDDL("HR", "ACTIVE_ORDERS")
if err != nil {
t.Fatal(err)
}
want := test.terminated
if test.noComments || test.tableError || test.columnError {
want = strings.TrimSpace(test.source)
} else {
if !test.columnOnly {
want += "\n\nCOMMENT ON TABLE \"HR\".\"ACTIVE_ORDERS\" IS 'View''s comment';"
}
if !test.tableOnly {
want += "\n\nCOMMENT ON COLUMN \"HR\".\"ACTIVE_ORDERS\".\"ID\" IS 'Column''s comment';"
}
}
if !strings.HasPrefix(want, "CREATE ") {
want = "CREATE OR REPLACE VIEW \"HR\".\"ACTIVE_ORDERS\" AS\n" + want
}
if got != want {
t.Fatalf("buildViewDDL() = %q, want %q", got, want)
}
if scripted.next != len(scripted.steps) {
t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next)
}
})
}
}
func TestGetTableDDLForViewAppendsComments(t *testing.T) {
const schema = "HR"
const view = "ACTIVE_ORDERS"
const viewText = `SELECT 1 AS "ID" FROM DUAL`
db, scripted := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{
{
queryContains: "FROM ALL_VIEWS",
args: []driver.Value{schema, view},
rows: [][]driver.Value{{viewText}},
},
{
queryContains: "FROM ALL_TAB_COMMENTS",
args: []driver.Value{schema, view},
rows: [][]driver.Value{{"Active orders"}},
},
{
queryContains: "FROM ALL_COL_COMMENTS",
args: []driver.Value{schema, view},
columns: []string{"COLUMN_NAME", "COMMENTS"},
rows: [][]driver.Value{{"ID", "Row id"}},
},
})
s := newServer()
s.db = db
got, err := s.getTableDDL(schema, view, "VIEW")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, `CREATE OR REPLACE VIEW "HR"."ACTIVE_ORDERS" AS`) {
t.Fatalf("expected view create DDL, got:\n%s", got)
}
if !strings.Contains(got, `COMMENT ON TABLE "HR"."ACTIVE_ORDERS" IS 'Active orders';`) {
t.Fatalf("expected view table comment, got:\n%s", got)
}
if !strings.Contains(got, `COMMENT ON COLUMN "HR"."ACTIVE_ORDERS"."ID" IS 'Row id';`) {
t.Fatalf("expected view column comment, got:\n%s", got)
}
if scripted.next != len(scripted.steps) {
t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next)
}
}
func TestGetPortableTableDDLDisablesAndRestoresSegmentAttributes(t *testing.T) {
const schema = "HR"
const table = "ORDERS"