fix(oracle): include dictionary comments in table and view DDL
This commit is contained in:
1 parent
84019b4271
commit
e00fbabb35
4 files changed
+791
-18
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user