feat(pg): install composite types in walk-through and query-span cata… · bytebase/bytebase@577afe6 · GitHub
Skip to content

Commit 577afe6

Browse files
h3n4lclaude
andcommitted
feat(pg): install composite types in walk-through and query-span catalogs
The walk-through (SQL review / task check) and query-span (masking, lineage) catalog loaders previously ignored composite_types metadata, so statements referencing composite types were checked against catalogs unaware of them. - Both loaders gain a composite object kind sharing the "type:" namespace with enums: real install via omni DefineCompositeType with attributes parsed from the always-qualified metadata type strings, dependency edges from attribute types (composite→composite and composite→enum), and the existing "_broken" pseudo composite as the install-failure fallback in query span - Walk-through diff application maps omni CompositeTypes diff entries back onto metadata (create/modify via catalog FormatType, drop by name), so post-DDL metadata reflects composite changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b512e2f commit 577afe6

9 files changed

Lines changed: 742 additions & 17 deletions
Lines changed: 183 additions & 0 deletions

‎backend/plugin/parser/pg/query_span_loader.go‎

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type objectKind int
5555
const (
5656
kindSchema objectKind = iota
5757
kindEnum
58+
kindComposite
5859
kindTable
5960
kindView
6061
kindMatView
@@ -71,11 +72,12 @@ type objectEntry struct {
7172
name string
7273

7374
// Exactly one of the following is populated based on kind.
74-
enumMeta *storepb.EnumTypeMetadata
75-
tableMeta *storepb.TableMetadata
76-
viewMeta *storepb.ViewMetadata
77-
matViewMeta *storepb.MaterializedViewMetadata
78-
funcMeta *storepb.FunctionMetadata
75+
enumMeta *storepb.EnumTypeMetadata
76+
compositeMeta *storepb.CompositeTypeMetadata
77+
tableMeta *storepb.TableMetadata
78+
viewMeta *storepb.ViewMetadata
79+
matViewMeta *storepb.MaterializedViewMetadata
80+
funcMeta *storepb.FunctionMetadata
7981
}
8082

8183
// key returns a canonical kind-prefixed identifier used for dependency
@@ -89,7 +91,7 @@ func (e *objectEntry) key() string {
8991
switch e.kind {
9092
case kindSchema:
9193
return "schema:" + e.schema
92-
case kindEnum:
94+
case kindEnum, kindComposite:
9395
return "type:" + e.schema + "." + e.name
9496
case kindTable, kindView, kindMatView:
9597
return "rel:" + e.schema + "." + e.name
@@ -130,6 +132,8 @@ func kindLabel(k objectKind) string {
130132
return "0schema"
131133
case kindEnum:
132134
return "1enum"
135+
case kindComposite:
136+
return "1composite"
133137
case kindTable:
134138
return "2table"
135139
case kindView:
@@ -205,6 +209,14 @@ func (l *catalogLoader) collectObjects() []*objectEntry {
205209
enumMeta: enum,
206210
})
207211
}
212+
for _, composite := range sm.CompositeTypes {
213+
out = append(out, &objectEntry{
214+
kind: kindComposite,
215+
schema: sm.Name,
216+
name: composite.Name,
217+
compositeMeta: composite,
218+
})
219+
}
208220
for _, tbl := range sm.Tables {
209221
out = append(out, &objectEntry{
210222
kind: kindTable,
@@ -378,6 +390,18 @@ func buildDependencyEdges(objects []*objectEntry, index map[string]*objectEntry)
378390
}
379391
}
380392
}
393+
case kindComposite:
394+
for _, attribute := range obj.compositeMeta.Attributes {
395+
for _, ref := range extractUserTypeRefs(attribute.Type) {
396+
if k := typeRefKey(ref); index[k] != nil {
397+
deps = append(deps, k)
398+
}
399+
// An attribute may be typed with a table/view row type.
400+
if k := relationKey(ref.Schema, ref.Name); index[k] != nil {
401+
deps = append(deps, k)
402+
}
403+
}
404+
}
381405
case kindSchema, kindEnum:
382406
// Schemas have no outgoing edges (they are the top of the graph).
383407
// Enums depend only on their schema, already captured above.
@@ -562,6 +586,12 @@ func (l *catalogLoader) installReal(obj *objectEntry) error {
562586
return err
563587
case kindEnum:
564588
return l.cat.DefineEnum(buildCreateEnumStmt(obj.schema, obj.enumMeta))
589+
case kindComposite:
590+
stmt, err := buildCompositeTypeStmt(obj.schema, obj.compositeMeta)
591+
if err != nil {
592+
return err
593+
}
594+
return l.cat.DefineCompositeType(stmt)
565595
case kindTable:
566596
stmt, err := buildCreateStmt(obj.schema, obj.tableMeta)
567597
if err != nil {
@@ -602,6 +632,27 @@ func (l *catalogLoader) installPseudo(obj *objectEntry) error {
602632
return errors.New("schema has no pseudo form")
603633
case kindEnum:
604634
return l.cat.DefineEnum(pseudoCreateEnumStmt(obj.schema, obj.name))
635+
case kindComposite:
636+
// Name-preserving, text-backed fallback when metadata attributes are
637+
// known, so (col).field access still resolves; the "_broken" shape
638+
// remains for composites with no attribute information at all.
639+
if len(obj.compositeMeta.GetAttributes()) > 0 {
640+
items := make([]ast.Node, 0, len(obj.compositeMeta.Attributes))
641+
for _, attribute := range obj.compositeMeta.Attributes {
642+
if attribute.Name == "" {
643+
continue
644+
}
645+
items = append(items, &ast.ColumnDef{
646+
Colname: attribute.Name,
647+
TypeName: pseudoTextTypeName(),
648+
})
649+
}
650+
return l.cat.DefineCompositeType(&ast.CompositeTypeStmt{
651+
Typevar: &ast.RangeVar{Schemaname: obj.schema, Relname: obj.name},
652+
Coldeflist: &ast.List{Items: items},
653+
})
654+
}
655+
return l.cat.DefineCompositeType(pseudoCompositeTypeStmt(obj.schema, obj.name))
605656
case kindTable:
606657
cols := columnNamesFromTableMetadata(obj.tableMeta)
607658
return l.cat.DefineRelation(pseudoCreateTableStmt(obj.schema, obj.name, cols), 'r')

‎backend/plugin/parser/pg/query_span_loader_builders.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,34 @@ func buildCreateEnumStmt(schema string, enum *storepb.EnumTypeMetadata) *ast.Cre
2828
}
2929
}
3030

31+
// buildCompositeTypeStmt translates CompositeTypeMetadata + schema into a
32+
// CompositeTypeStmt. Attribute types are parsed via typeNameFromString; an
33+
// error on any attribute propagates so the loader can pseudo the whole type.
34+
func buildCompositeTypeStmt(schema string, composite *storepb.CompositeTypeMetadata) (*ast.CompositeTypeStmt, error) {
35+
cols := make([]ast.Node, 0, len(composite.Attributes))
36+
for _, attribute := range composite.Attributes {
37+
if attribute.Name == "" {
38+
continue
39+
}
40+
if attribute.Type == "" {
41+
return nil, errors.Errorf("attribute %q: empty type", attribute.Name)
42+
}
43+
tn, err := typeNameFromString(attribute.Type)
44+
if err != nil {
45+
return nil, errors.Wrapf(err, "attribute %q", attribute.Name)
46+
}
47+
cols = append(cols, &ast.ColumnDef{
48+
Colname: attribute.Name,
49+
TypeName: tn,
50+
CollClause: qsCollateClauseFromReference(attribute.Collation),
51+
})
52+
}
53+
return &ast.CompositeTypeStmt{
54+
Typevar: &ast.RangeVar{Schemaname: schema, Relname: composite.Name},
55+
Coldeflist: &ast.List{Items: cols},
56+
}, nil
57+
}
58+
3159
// buildCreateStmt translates TableMetadata into a CreateStmt with real
3260
// column types. Column types are parsed via typeNameFromString; an error on
3361
// any single column propagates back so the loader can pseudo the whole
@@ -243,3 +271,21 @@ func splitTopLevelCommas(s string) []string {
243271
parts = append(parts, strings.TrimSpace(s[start:]))
244272
return parts
245273
}
274+
275+
// qsCollateClauseFromReference converts a stored emit-ready collation
276+
// reference (quoted as needed, schema-qualified outside pg_catalog) into an
277+
// AST collate clause. Returns nil for no collation.
278+
func qsCollateClauseFromReference(reference string) *ast.CollateClause {
279+
if reference == "" {
280+
return nil
281+
}
282+
var items []ast.Node
283+
if schemaName, collationName, ok := splitQualifiedName(reference); ok {
284+
items = []ast.Node{&ast.String{Str: schemaName}, &ast.String{Str: collationName}}
285+
} else {
286+
name := strings.Trim(reference, `"`)
287+
name = strings.ReplaceAll(name, `""`, `"`)
288+
items = []ast.Node{&ast.String{Str: name}}
289+
}
290+
return &ast.CollateClause{Collname: &ast.List{Items: items}}
291+
}

‎backend/plugin/parser/pg/query_span_loader_pseudo.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ func pseudoCreateDomainStmt(schema, name string) *ast.CreateDomainStmt {
4848
}
4949

5050
// pseudoCompositeTypeStmt builds a composite type with a single text field
51-
// named "_broken". Without CompositeTypeMetadata in storepb we cannot
52-
// preserve real field names, so (col).field access against a pseudo composite
53-
// will fall through to extractFallbackColumns.
51+
// named "_broken". It is the fallback when a composite type's real
52+
// definition is absent from metadata or fails to install, so (col).field
53+
// access against a pseudo composite will fall through to
54+
// extractFallbackColumns.
5455
func pseudoCompositeTypeStmt(schema, name string) *ast.CompositeTypeStmt {
5556
return &ast.CompositeTypeStmt{
5657
Typevar: &ast.RangeVar{

‎backend/plugin/parser/pg/query_span_loader_type_name.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ func extractUserTypeRefs(typeStr string) []UserTypeRef {
9191
// the caller's job to split.
9292
func stripTypeModifiers(typeStr string) string {
9393
s := strings.TrimSpace(typeStr)
94+
// Array suffixes wrap the element type: "public.addr[]" refers to
95+
// public.addr. Quoted identifiers end with a quote, so a genuine name
96+
// containing "[]" is never clipped.
97+
for strings.HasSuffix(s, "[]") {
98+
s = strings.TrimSpace(strings.TrimSuffix(s, "[]"))
99+
}
94100
s = stripTimeZoneSuffix(s)
95101
s = stripParens(s)
96102
return strings.TrimSpace(s)

‎backend/plugin/parser/pg/query_span_omni.go‎

Lines changed: 5 additions & 0 deletions

0 commit comments

Comments
 (0)