Skip to content

Commit c407ef6

Browse files
luongs3claude
andcommitted
fix(postgresql): DROP TABLE ... CASCADE drops dependent views (#4416)
sqlc's in-memory catalog dropped the table but left views that depended on it, so a later CREATE VIEW reusing the name failed with 'relation "..." already exists' even though the SQL is valid Postgres. - ast: carry DROP behavior (RESTRICT/CASCADE) into DropTableStmt; add named DropBehavior constants matching the pg_query enum. - postgresql parser: thread n.Behavior into the DropTableStmt. - catalog: record each view's referenced tables (DependsOn) at create time, and on DROP TABLE ... CASCADE evict dependent views transitively. - RESTRICT / unspecified behavior is unchanged (views are kept). - tests: catalog unit tests covering cascade eviction (incl. views on views) and the restrict/default keep-path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a3b0cfd commit c407ef6

6 files changed

Lines changed: 172 additions & 4 deletions

File tree

internal/engine/postgresql/parse.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ func translate(node *nodes.Node) (ast.Node, error) {
579579
case nodes.ObjectType_OBJECT_TABLE, nodes.ObjectType_OBJECT_VIEW, nodes.ObjectType_OBJECT_MATVIEW:
580580
drop := &ast.DropTableStmt{
581581
IfExists: n.MissingOk,
582+
Behavior: ast.DropBehavior(n.Behavior),
582583
}
583584
for _, obj := range n.Objects {
584585
name, err := parseRelation(obj)

internal/sql/ast/drop_behavior.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ package ast
22

33
type DropBehavior uint
44

5+
// Matches pganalyze/pg_query_go DropBehavior enum:
6+
// DropBehavior_UNDEFINED = 0, DROP_RESTRICT = 1, DROP_CASCADE = 2.
7+
const (
8+
DropBehaviorUndefined DropBehavior = 0
9+
DropBehaviorRestrict DropBehavior = 1
10+
DropBehaviorCascade DropBehavior = 2
11+
)
12+
513
func (n *DropBehavior) Pos() int {
614
return 0
715
}

internal/sql/ast/drop_table_stmt.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ast
33
type DropTableStmt struct {
44
IfExists bool
55
Tables []*TableName
6+
Behavior DropBehavior
67
}
78

89
func (n *DropTableStmt) Pos() int {

internal/sql/catalog/table.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import (
1313
// A database table is a collection of related data held in a table format within a database.
1414
// It consists of columns and rows.
1515
type Table struct {
16-
Rel *ast.TableName
17-
Columns []*Column
18-
Comment string
16+
Rel *ast.TableName
17+
Columns []*Column
18+
Comment string
19+
DependsOn []*ast.TableName // for views: tables/views referenced by the view query
1920
}
2021

2122
func checkMissing(err error, missingOK bool) error {
@@ -384,11 +385,43 @@ func (c *Catalog) dropTable(stmt *ast.DropTableStmt) error {
384385
return err
385386
}
386387

388+
droppedName := schema.Tables[idx].Rel.Name
387389
schema.Tables = append(schema.Tables[:idx], schema.Tables[idx+1:]...)
390+
391+
if stmt.Behavior == ast.DropBehaviorCascade {
392+
c.dropDependentViews(schema, droppedName)
393+
}
388394
}
389395
return nil
390396
}
391397

398+
// dropDependentViews removes every view in schema whose DependsOn references
399+
// name, recursing so views-on-views are also evicted. Cascade-only.
400+
func (c *Catalog) dropDependentViews(schema *Schema, name string) {
401+
for {
402+
removed := false
403+
for i, t := range schema.Tables {
404+
depends := false
405+
for _, d := range t.DependsOn {
406+
if d.Name == name {
407+
depends = true
408+
break
409+
}
410+
}
411+
if depends {
412+
victim := schema.Tables[i].Rel.Name
413+
schema.Tables = append(schema.Tables[:i], schema.Tables[i+1:]...)
414+
c.dropDependentViews(schema, victim)
415+
removed = true
416+
break
417+
}
418+
}
419+
if !removed {
420+
return
421+
}
422+
}
423+
}
424+
392425
func (c *Catalog) renameColumn(stmt *ast.RenameColumnStmt) error {
393426
_, tbl, err := c.getTable(stmt.Table)
394427
if err != nil {

internal/sql/catalog/table_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package catalog_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
8+
"github.com/sqlc-dev/sqlc/internal/sql/ast"
9+
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
10+
)
11+
12+
// stubColumnGenerator satisfies the catalog's column generator dependency
13+
// without pulling in the full compiler. CREATE VIEW only needs a column set
14+
// to store; these tests assert on relation presence and dependency tracking,
15+
// not on the view's column types.
16+
type stubColumnGenerator struct{}
17+
18+
func (stubColumnGenerator) OutputColumns(ast.Node) ([]*catalog.Column, error) {
19+
return []*catalog.Column{
20+
{Name: "id", Type: ast.TypeName{Name: "int4"}},
21+
}, nil
22+
}
23+
24+
func update(t *testing.T, c *catalog.Catalog, sql string) {
25+
t.Helper()
26+
stmts, err := postgresql.NewParser().Parse(strings.NewReader(sql))
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
for _, stmt := range stmts {
31+
if err := c.Update(stmt, stubColumnGenerator{}); err != nil {
32+
t.Fatal(err)
33+
}
34+
}
35+
}
36+
37+
func publicSchema(t *testing.T, c *catalog.Catalog) *catalog.Schema {
38+
t.Helper()
39+
for _, s := range c.Schemas {
40+
if s.Name == "public" {
41+
return s
42+
}
43+
}
44+
t.Fatal(`schema "public" not found`)
45+
return nil
46+
}
47+
48+
func tableNames(schema *catalog.Schema) []string {
49+
names := make([]string, 0, len(schema.Tables))
50+
for _, tbl := range schema.Tables {
51+
names = append(names, tbl.Rel.Name)
52+
}
53+
return names
54+
}
55+
56+
func TestDropTableCascadeEvictsDependentViews(t *testing.T) {
57+
c := catalog.New("public")
58+
update(t, c, `
59+
CREATE TABLE base (id int);
60+
CREATE VIEW child AS SELECT id FROM base;
61+
CREATE VIEW grandchild AS SELECT id FROM child;
62+
`)
63+
64+
schema := publicSchema(t, c)
65+
if got := tableNames(schema); len(got) != 3 {
66+
t.Fatalf("expected 3 relations before drop, got %v", got)
67+
}
68+
69+
update(t, c, `DROP TABLE base CASCADE;`)
70+
71+
// base is dropped; child depends on base and grandchild depends on child,
72+
// so CASCADE must transitively evict both views.
73+
if got := tableNames(schema); len(got) != 0 {
74+
t.Fatalf("expected cascade drop to remove base and dependent views, got %v", got)
75+
}
76+
}
77+
78+
func TestDropTableWithoutCascadeKeepsDependentViews(t *testing.T) {
79+
for _, tc := range []struct {
80+
name string
81+
sql string
82+
}{
83+
{name: "restrict", sql: `DROP TABLE base RESTRICT;`},
84+
{name: "default", sql: `DROP TABLE base;`},
85+
} {
86+
t.Run(tc.name, func(t *testing.T) {
87+
c := catalog.New("public")
88+
update(t, c, `
89+
CREATE TABLE base (id int);
90+
CREATE VIEW child AS SELECT id FROM base;
91+
`)
92+
93+
schema := publicSchema(t, c)
94+
update(t, c, tc.sql)
95+
96+
got := tableNames(schema)
97+
if len(got) != 1 || got[0] != "child" {
98+
t.Fatalf("expected dependent view to remain after %s, got %v", tc.name, got)
99+
}
100+
if len(schema.Tables[0].DependsOn) != 1 || schema.Tables[0].DependsOn[0].Name != "base" {
101+
t.Fatalf("expected remaining view dependency to be preserved, got %#v", schema.Tables[0].DependsOn)
102+
}
103+
})
104+
}
105+
}

internal/sql/catalog/view.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package catalog
22

33
import (
44
"github.com/sqlc-dev/sqlc/internal/sql/ast"
5+
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
56
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
67
)
78

@@ -20,13 +21,32 @@ func (c *Catalog) createView(stmt *ast.ViewStmt, colGen columnGenerator) error {
2021
schemaName = *stmt.View.Schemaname
2122
}
2223

24+
var dependsOn []*ast.TableName
25+
list := astutils.Search(stmt.Query, func(node ast.Node) bool {
26+
_, ok := node.(*ast.RangeVar)
27+
return ok
28+
})
29+
for _, item := range list.Items {
30+
if rv, ok := item.(*ast.RangeVar); ok && rv.Relname != nil {
31+
tn := &ast.TableName{Name: *rv.Relname}
32+
if rv.Schemaname != nil {
33+
tn.Schema = *rv.Schemaname
34+
}
35+
if rv.Catalogname != nil {
36+
tn.Catalog = *rv.Catalogname
37+
}
38+
dependsOn = append(dependsOn, tn)
39+
}
40+
}
41+
2342
tbl := Table{
2443
Rel: &ast.TableName{
2544
Catalog: catName,
2645
Schema: schemaName,
2746
Name: *stmt.View.Relname,
2847
},
29-
Columns: cols,
48+
Columns: cols,
49+
DependsOn: dependsOn,
3050
}
3151

3252
ns := tbl.Rel.Schema

0 commit comments

Comments
 (0)