Support DROP TABLE ... CASCADE and RESTRICT - #3155
Conversation
Fixes #3120. DROP TABLE ... CASCADE previously failed with 'CASCADE is not yet supported' (and RESTRICT with a similar error). RESTRICT is the default behavior in Postgres, so it is now handled by the standard DROP TABLE path. CASCADE converts to a new DropTableCascade node that first drops the foreign key constraints on other tables that reference the dropped tables (via the same sql.ForeignKeyTable interface calls the engine's DROP TABLE execution uses), then delegates the actual drop to the standard DROP TABLE path, which keeps its existing dependency bookkeeping such as dropping sequences owned by the tables' columns. Table names without an explicit schema are resolved against the search path, and IF EXISTS skips missing tables as before. Not yet cascaded (dependency tracking does not cover them yet): views that reference a dropped table (they are left in place, matching the behavior of plain DROP TABLE), and tables/functions/procedures using a dropped table's row type as a column or parameter type (these still error, as without CASCADE).
|
SummaryCoverage spans normal and edge-case table removal behavior, including dependency handling, cascading cleanup, rollback safety, concurrent operations, qualified and quoted names, schema resolution, metadata consistency, and preservation of related data and objects. It also checks that invalid operations fail safely without damaging existing protections or usability. Safe to merge — the run found no PR-attributable regressions or new failures across the exercised database behaviors, and all checks passed. No merge blocker was identified. Tests run by Ito
Tip Reply with @itoqa to send us feedback on this test run. |
|
|
@coffeegoddd DOLT
|
Replaces the DropTableCascade node with a Cascade flag plumbed through vitess and GMS to plan.DropTable. The BeforeTableDeletion hook now performs the cascade at execution time: dependent views (transitively, resolved the same way the engine resolves view definitions), foreign keys on other tables referencing the dropped tables, functions and procedures with parameters of a dropped table's row type, and columns of other tables using a dropped table's row type.
|
Diff SummaryCoverage spans schema deletion and dependency handling, data import/export round trips, malformed-input recovery, startup protection, integrity repair, and trigger or record behavior across both normal and adversarial cases. The broader health is mixed: many supported workflows behave correctly, but table deletion can still remove objects unsafely or leave dependent constraints and routines behind. Not safe to merge yet — this PR has a high-severity deletion-safety failure that can remove a table while leaving an unusable dependent object, along with regressions and new failures in foreign-key cleanup and restricted deletion behavior. These are directly attributable to the changed schema-management logic and represent a merge blocker, not a flag-for-later caveat. Tests run by ItoTests that are no longer relevantBelow are tests that previously ran and are no longer relevant:
Tip Reply with @itoqa to send us feedback on this test run. |
| TargetType: auth.AuthTargetType_TableIdentifiers, | ||
| TargetNames: authTableNames, | ||
| } | ||
| return &vitess.DDL{ |
There was a problem hiding this comment.
🆕 New Failure: identified in this diff run
Cascade leaves foreign keys behind
What failed: The command returned a dependency error for child_middle_id_fkey. The metadata still contained foreign keys for child, middle, and survivor, and parent, middle, child, and survivor all remained instead of the dropped tables being removed.
Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
- Severity: Medium
- Impact: Database users cannot remove related tables with CASCADE when a foreign key is held by a table that remains. The tables stay in place, so schema changes and cleanup work are blocked until the constraints are handled another way.
- Steps to Reproduce:
- Create parent, middle, and child tables where middle references parent and child references middle.
- Add additional foreign keys and a surviving table that reference the tables being dropped.
- Run DROP TABLE parent, middle CASCADE.
- Inspect the foreign-key metadata and then reconnect to try an insert into the surviving child table.
- Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
- Code Analysis: The PR routes DROP TABLE through the standard DDL node in server/ast/drop_table.go:45-54 and sets Cascade from tree.DropCascade at line 52. servercfg/config.go:50-56 registers hook.BeforeTableDeletion as the DropTable pre-execution hook. In server/hook/delete_table.go:39-68, the CASCADE branch calls cascadeDropDependencies before the standard dependency checks. The new server/hook/delete_table_cascade.go:216-254 implementation obtains the foreign-key collection, finds keys referencing each table in the drop set, skips only keys whose declaring table is also in the drop set, and calls ForeignKeyTable.DropForeignKey for keys declared by surviving tables. In the recorded multi-table execution, that cleanup did not remove child_middle_id_fkey: standard validation then rejected the drop and the catalog retained all relations. The smallest practical fix is to correct the foreign-key collection/table resolution or DropForeignKey invocation in cascadeDropForeignKeys so every foreign key declared by a surviving table and referencing any requested drop target is removed before standard validation; keep the existing in-drop-set skip for constraints owned by tables that will be deleted. Add the BF-CROSS-3 multi-table/transitive regression case to prevent this path from regressing.
- Why this is likely a bug: This is a real application failure rather than a browser or setup issue: local PostgreSQL-wire execution of valid SQL returned the engine's foreign-key dependency error, and the post-failure catalog query showed that the relations and foreign keys remained. The expected behavior is also encoded in testing/go/drop_table_test.go:206-230, where dropping parent and middle must succeed and an insert that would have violated the removed child-to-middle key must succeed. The PR introduced the CASCADE flag, pre-hook, and foreign-key cleanup path, so the failed cleanup is directly within the changed feature surface. A targeted correction to the lookup or DropForeignKey call in cascadeDropForeignKeys is sufficient; no broad redesign is required.
Relevant code
server/ast/drop_table.go:45-54
return &vitess.DDL{ Action: vitess.DropStr, FromTables: tableNames, IfExists: node.IfExists, Cascade: node.DropBehavior == tree.DropCascade, Auth: authInformation }server/hook/delete_table.go:50-55
if n.Cascade {
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}server/hook/delete_table_cascade.go:228-251
referencedByFk := fkc.KeysForTable(tblName)
...
if tableNameInSet(fk.TableName, allDeletedTables) { continue }
if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil { return err }testing/go/drop_table_test.go:206-230
Query: `DROP TABLE parent, middle CASCADE;`
...
Query: `INSERT INTO child VALUES (1, 99);`, Expected: []sql.Row{}Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.
**Medium severity — Cascade leaves foreign keys behind**
**What failed:** The command returned a dependency error for child_middle_id_fkey. The metadata still contained foreign keys for child, middle, and survivor, and parent, middle, child, and survivor all remained instead of the dropped tables being removed.
- **Impact:** Database users cannot remove related tables with CASCADE when a foreign key is held by a table that remains. The tables stay in place, so schema changes and cleanup work are blocked until the constraints are handled another way.
- **Steps to reproduce:**
1. Create parent, middle, and child tables where middle references parent and child references middle.
2. Add additional foreign keys and a surviving table that reference the tables being dropped.
3. Run DROP TABLE parent, middle CASCADE.
4. Inspect the foreign-key metadata and then reconnect to try an insert into the surviving child table.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The PR routes DROP TABLE through the standard DDL node in server/ast/drop_table.go:45-54 and sets Cascade from tree.DropCascade at line 52. servercfg/config.go:50-56 registers hook.BeforeTableDeletion as the DropTable pre-execution hook. In server/hook/delete_table.go:39-68, the CASCADE branch calls cascadeDropDependencies before the standard dependency checks. The new server/hook/delete_table_cascade.go:216-254 implementation obtains the foreign-key collection, finds keys referencing each table in the drop set, skips only keys whose declaring table is also in the drop set, and calls ForeignKeyTable.DropForeignKey for keys declared by surviving tables. In the recorded multi-table execution, that cleanup did not remove child_middle_id_fkey: standard validation then rejected the drop and the catalog retained all relations. The smallest practical fix is to correct the foreign-key collection/table resolution or DropForeignKey invocation in cascadeDropForeignKeys so every foreign key declared by a surviving table and referencing any requested drop target is removed before standard validation; keep the existing in-drop-set skip for constraints owned by tables that will be deleted. Add the BF-CROSS-3 multi-table/transitive regression case to prevent this path from regressing.
- **Why this is likely a bug:** This is a real application failure rather than a browser or setup issue: local PostgreSQL-wire execution of valid SQL returned the engine's foreign-key dependency error, and the post-failure catalog query showed that the relations and foreign keys remained. The expected behavior is also encoded in testing/go/drop_table_test.go:206-230, where dropping parent and middle must succeed and an insert that would have violated the removed child-to-middle key must succeed. The PR introduced the CASCADE flag, pre-hook, and foreign-key cleanup path, so the failed cleanup is directly within the changed feature surface. A targeted correction to the lookup or DropForeignKey call in cascadeDropForeignKeys is sufficient; no broad redesign is required.
**Relevant code:**
`server/ast/drop_table.go:45-54`
~~~go
return &vitess.DDL{ Action: vitess.DropStr, FromTables: tableNames, IfExists: node.IfExists, Cascade: node.DropBehavior == tree.DropCascade, Auth: authInformation }
~~~
`server/hook/delete_table.go:50-55`
~~~go
if n.Cascade {
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}
~~~
`server/hook/delete_table_cascade.go:228-251`
~~~go
referencedByFk := fkc.KeysForTable(tblName)
...
if tableNameInSet(fk.TableName, allDeletedTables) { continue }
if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil { return err }
~~~
`testing/go/drop_table_test.go:206-230`
~~~go
Query: `DROP TABLE parent, middle CASCADE;`
...
Query: `INSERT INTO child VALUES (1, 99);`, Expected: []sql.Row{}
~~~There was a problem hiding this comment.
Interesting that this is saying foreign keys aren't being dropped
| TargetType: auth.AuthTargetType_TableIdentifiers, | ||
| TargetNames: authTableNames, | ||
| } | ||
| return &vitess.DDL{ |
There was a problem hiding this comment.
🔁 Regression: previously passing at 94bfd08
Cascade cannot remove external foreign key
What failed: The database rejected DROP TABLE parent CASCADE because the foreign key on child was still present. CASCADE should remove that constraint, drop parent, keep child, and allow child rows that no longer reference an existing parent.
Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
- Severity: Medium
- Impact: When a user drops a parent table with CASCADE, the command fails and leaves both tables and the foreign-key constraint in place. They must remove the constraint separately before the table can be dropped.
- Steps to Reproduce:
- Create a parent table with an integer primary key.
- Create a child table with a foreign key that references the parent key.
- Insert one parent row and one matching child row.
- Run DROP TABLE parent CASCADE.
- Observe that the statement fails with a dependency error naming the child's foreign-key constraint, and both tables remain.
- Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
- Code Analysis: The PostgreSQL AST conversion in server/ast/drop_table.go:45-53 maps DROP TABLE ... CASCADE to the standard vitess DDL DropTable node with Cascade=true. The PR wires that node into hook.BeforeTableDeletion through servercfg/config.go:54-56. In server/hook/delete_table.go:50-55, the pre-hook calls cascadeDropDependencies before the standard dependency checks and table drop. The new cascadeDropDependencies function in server/hook/delete_table_cascade.go:42-52 delegates foreign-key cleanup to cascadeDropForeignKeys. That function obtains the foreign-key collection at lines 219-225, asks for keys referencing each target at line 229, resolves the surviving table at lines 233-240, and calls DropForeignKey at lines 244-251. The observed error proves this path did not remove child_parent_id_fkey before standard validation; the standard path then correctly refuses to drop the still-referenced parent. The repository's own integration contract in testing/go/drop_table_test.go:114-153 expects the opposite behavior: the parent disappears, the child remains, and an insert violating the former foreign key succeeds. The targeted fix is to correct the lookup or removal arguments in cascadeDropForeignKeys so every external key returned for the dropped table is removed before validation, while retaining keys for tables that are themselves being dropped.
- Why this is likely a bug: This is a reproducible local database failure, not a browser or connection observation: PostgreSQL reports that child_parent_id_fkey still references parent, and the parent and child remain. The requested CASCADE behavior has a precise product contract in the repository integration test, which requires removing only the external foreign key while preserving the child table. The implementation added by this PR is specifically responsible for performing that cleanup, yet its cleanup result is absent before standard dependency validation. A targeted correction to the foreign-key collection lookup or DropForeignKey call can address the failure without changing unrelated drop behavior.
Relevant code
server/ast/drop_table.go:45-53
return &vitess.DDL{
Action: vitess.DropStr,
FromTables: tableNames,
IfExists: node.IfExists,
Cascade: node.DropBehavior == tree.DropCascade,
Auth: authInformation,
}, nilservercfg/config.go:50-56
Hooks: sql.ExecutionHooks{
DropTable: sql.DropTable{
PreSQLExecution: hook.BeforeTableDeletion,
},
},server/hook/delete_table.go:50-55
if n.Cascade {
// CASCADE drops the objects that depend on the dropped tables before the standard drop path runs.
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}server/hook/delete_table_cascade.go:216-253
func cascadeDropForeignKeys(ctx *sql.Context, allDeletedTables []doltdb.TableName) error {
_, root, err := core.GetRootFromContext(ctx)
...
_, referencedByFk := fkc.KeysForTable(tblName)
...
for _, fk := range referencedByFk {
if tableNameInSet(fk.TableName, allDeletedTables) {
continue
}
if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil {
return err
}
}
return nil
}testing/go/drop_table_test.go:133-153
Query: `DROP TABLE parent CASCADE;`,
Expected: []sql.Row{},
...
Query: `SELECT * FROM child;`,
Expected: []sql.Row{{10, 1}},
...
Query: `INSERT INTO child VALUES (11, 99);`,
Expected: []sql.Row{},Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.
**Medium severity — Cascade cannot remove external foreign key**
**What failed:** The database rejected DROP TABLE parent CASCADE because the foreign key on child was still present. CASCADE should remove that constraint, drop parent, keep child, and allow child rows that no longer reference an existing parent.
- **Impact:** When a user drops a parent table with CASCADE, the command fails and leaves both tables and the foreign-key constraint in place. They must remove the constraint separately before the table can be dropped.
- **Steps to reproduce:**
1. Create a parent table with an integer primary key.
2. Create a child table with a foreign key that references the parent key.
3. Insert one parent row and one matching child row.
4. Run DROP TABLE parent CASCADE.
5. Observe that the statement fails with a dependency error naming the child's foreign-key constraint, and both tables remain.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The PostgreSQL AST conversion in server/ast/drop_table.go:45-53 maps DROP TABLE ... CASCADE to the standard vitess DDL DropTable node with Cascade=true. The PR wires that node into hook.BeforeTableDeletion through servercfg/config.go:54-56. In server/hook/delete_table.go:50-55, the pre-hook calls cascadeDropDependencies before the standard dependency checks and table drop. The new cascadeDropDependencies function in server/hook/delete_table_cascade.go:42-52 delegates foreign-key cleanup to cascadeDropForeignKeys. That function obtains the foreign-key collection at lines 219-225, asks for keys referencing each target at line 229, resolves the surviving table at lines 233-240, and calls DropForeignKey at lines 244-251. The observed error proves this path did not remove child_parent_id_fkey before standard validation; the standard path then correctly refuses to drop the still-referenced parent. The repository's own integration contract in testing/go/drop_table_test.go:114-153 expects the opposite behavior: the parent disappears, the child remains, and an insert violating the former foreign key succeeds. The targeted fix is to correct the lookup or removal arguments in cascadeDropForeignKeys so every external key returned for the dropped table is removed before validation, while retaining keys for tables that are themselves being dropped.
- **Why this is likely a bug:** This is a reproducible local database failure, not a browser or connection observation: PostgreSQL reports that child_parent_id_fkey still references parent, and the parent and child remain. The requested CASCADE behavior has a precise product contract in the repository integration test, which requires removing only the external foreign key while preserving the child table. The implementation added by this PR is specifically responsible for performing that cleanup, yet its cleanup result is absent before standard dependency validation. A targeted correction to the foreign-key collection lookup or DropForeignKey call can address the failure without changing unrelated drop behavior.
**Relevant code:**
`server/ast/drop_table.go:45-53`
~~~go
return &vitess.DDL{
Action: vitess.DropStr,
FromTables: tableNames,
IfExists: node.IfExists,
Cascade: node.DropBehavior == tree.DropCascade,
Auth: authInformation,
}, nil
~~~
`servercfg/config.go:50-56`
~~~go
Hooks: sql.ExecutionHooks{
DropTable: sql.DropTable{
PreSQLExecution: hook.BeforeTableDeletion,
},
},
~~~
`server/hook/delete_table.go:50-55`
~~~go
if n.Cascade {
// CASCADE drops the objects that depend on the dropped tables before the standard drop path runs.
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}
~~~
`server/hook/delete_table_cascade.go:216-253`
~~~go
func cascadeDropForeignKeys(ctx *sql.Context, allDeletedTables []doltdb.TableName) error {
_, root, err := core.GetRootFromContext(ctx)
...
_, referencedByFk := fkc.KeysForTable(tblName)
...
for _, fk := range referencedByFk {
if tableNameInSet(fk.TableName, allDeletedTables) {
continue
}
if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil {
return err
}
}
return nil
}
~~~
`testing/go/drop_table_test.go:133-153`
~~~go
Query: `DROP TABLE parent CASCADE;`,
Expected: []sql.Row{},
...
Query: `SELECT * FROM child;`,
Expected: []sql.Row{{10, 1}},
...
Query: `INSERT INTO child VALUES (11, 99);`,
Expected: []sql.Row{},
~~~| allTableNames = append(allTableNames, doltTable.TableName()) | ||
| } | ||
| // TODO: handle DROP TABLE CASCADE | ||
| if n.Cascade { |
There was a problem hiding this comment.
🆕 New Failure: identified in this diff run
Cascade drops a table but leaves its function
What failed: DROP TABLE target6 CASCADE completed successfully and removed target6, while the dependent function unsupported6 remained in the catalog. The expected behavior was to reject the drop because this dependency is not handled, or to remove the dependent routine as part of CASCADE.
Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
- Severity: High
- Impact: Database administrators can remove a table while a dependent function remains behind, leaving the schema inconsistent and that function unusable. The command does not stop to warn them about the unsupported dependency.
- Steps to Reproduce:
- Create a table named target6 with an integer column.
- Create a SQL function named unsupported6 that returns target6 and selects one row from target6.
- Run DROP TABLE target6 CASCADE.
- Check the catalog for target6 and unsupported6. The table is gone, but the function remains instead of the drop being rejected.
- Stub / mock content: The test used an isolated local database and ordinary SQL objects. No stubs, mocks, or bypasses were applied for this test in the recorded run.
- Code Analysis: The standard AST conversion in server/ast/drop_table.go lines 45-53 maps DROP TABLE ... CASCADE to the standard vitess DDL node with Cascade=true. The PR diff removed the previous custom cascade node and changed server/hook/delete_table.go lines 50-55 to call cascadeDropDependencies before the standard drop path. In the new server/hook/delete_table_cascade.go, cascadeDropRoutines at lines 257-299 builds deletedTypes, iterates functions at lines 264-273, and marks a function only when one of f.AllParams has a deleted table type at lines 265-270. It never compares f.ReturnType with deletedTypes, even though core/functions/collection.go lines 40-54 shows Function stores ReturnType separately from AllParams. The later remaining-dependency validation in server/hook/delete_table.go lines 56-67 calls beforeTableDeletionCheckFuncsProcs, which also iterates only f.AllParams at lines 133-141 and therefore cannot catch a return-type dependency. With a function declared RETURNS target6 and no target6 parameter, both checks miss the dependency and the standard drop removes the table while retaining the function. The smallest practical fix is to treat a function or procedure return type that matches a deleted table row type as a dependency: either include ReturnType in cascadeDropRoutines' deletion check, or include it in the remaining-dependency validation if the intended behavior is to reject unsupported return-type cleanup. The chosen behavior should be covered by a regression test using a routine with RETURNS the_table.
- Why this is likely a bug: The database result is specific and reproducible: target6 was absent after DROP TABLE target6 CASCADE, while unsupported6 was still present. This is not a browser or mock artifact; the SQL setup created a real table and function, and the source path independently explains the exact mismatch. A function's ReturnType is persisted as a separate field, but the PR's new cleanup and blocking checks inspect only parameter types, so a return-type dependency falls through to table deletion. Leaving a routine that references a deleted row type makes the catalog inconsistent and violates the test's dependency-safety contract. The targeted remediation is to inspect return types in the routine dependency handling and either drop those routines during CASCADE or reject the drop when that dependency is not supported.
Relevant code
server/hook/delete_table.go:50-67
if n.Cascade {
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}
for _, doltTable := range resolvedTables {
if err := beforeTableDeletionCheckTableColumns(ctx, doltTable, allTableNames); err != nil {
return nil, err
}
if err := beforeTableDeletionCheckFuncsProcs(ctx, doltTable, allTableNames); err != nil {server/hook/delete_table_cascade.go:257-279
func cascadeDropRoutines(ctx *sql.Context, allDeletedTables []doltdb.TableName) error {
deletedTypes := deletedTableTypes(allDeletedTables)
funcsColl, err := core.GetFunctionsCollectionFromContext(ctx, "")
...
err = funcsColl.IterateFunctions(ctx, func(f functions.Function) (stop bool, err error) {
for _, param := range f.AllParams {
if _, ok := deletedTypes[param.Type]; ok {
funcIDs = append(funcIDs, f.ID)
break
}
}
return false, nil
})
...
if err = funcsColl.DropFunction(ctx, funcIDs...); err != nil {core/functions/collection.go:40-54
type Function struct {
ID id.Function
ReturnType id.Type
AllParams []procedures.Parameter
Variadic bool
IsNonDeterministic bool
Strict bool
Definition string
ExtensionName string
ExtensionSymbol string
Operations []plpgsql.InterpreterOperation
SQLDefinition string
SetOf bool
}server/hook/delete_table.go:126-141
func beforeTableDeletionCheckFuncsProcs(ctx *sql.Context, doltTable *sqle.DoltTable, allDeletedTables []doltdb.TableName) error {
tableName := doltTable.TableName()
tableAsType := id.NewType(tableName.Schema, tableName.Name)
...
err = funcsColl.IterateFunctions(ctx, func(f functions.Function) (stop bool, err error) {
for _, param := range f.AllParams {
if param.Type == tableAsType {
return true, errors.Newf("cannot drop table %s because other objects depend on it ...")Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.
**High severity — Cascade drops a table but leaves its function**
**What failed:** DROP TABLE target6 CASCADE completed successfully and removed target6, while the dependent function unsupported6 remained in the catalog. The expected behavior was to reject the drop because this dependency is not handled, or to remove the dependent routine as part of CASCADE.
- **Impact:** Database administrators can remove a table while a dependent function remains behind, leaving the schema inconsistent and that function unusable. The command does not stop to warn them about the unsupported dependency.
- **Steps to reproduce:**
1. Create a table named target6 with an integer column.
2. Create a SQL function named unsupported6 that returns target6 and selects one row from target6.
3. Run DROP TABLE target6 CASCADE.
4. Check the catalog for target6 and unsupported6. The table is gone, but the function remains instead of the drop being rejected.
- **Stub / mock content:** The test used an isolated local database and ordinary SQL objects. No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The standard AST conversion in server/ast/drop_table.go lines 45-53 maps DROP TABLE ... CASCADE to the standard vitess DDL node with Cascade=true. The PR diff removed the previous custom cascade node and changed server/hook/delete_table.go lines 50-55 to call cascadeDropDependencies before the standard drop path. In the new server/hook/delete_table_cascade.go, cascadeDropRoutines at lines 257-299 builds deletedTypes, iterates functions at lines 264-273, and marks a function only when one of f.AllParams has a deleted table type at lines 265-270. It never compares f.ReturnType with deletedTypes, even though core/functions/collection.go lines 40-54 shows Function stores ReturnType separately from AllParams. The later remaining-dependency validation in server/hook/delete_table.go lines 56-67 calls beforeTableDeletionCheckFuncsProcs, which also iterates only f.AllParams at lines 133-141 and therefore cannot catch a return-type dependency. With a function declared RETURNS target6 and no target6 parameter, both checks miss the dependency and the standard drop removes the table while retaining the function. The smallest practical fix is to treat a function or procedure return type that matches a deleted table row type as a dependency: either include ReturnType in cascadeDropRoutines' deletion check, or include it in the remaining-dependency validation if the intended behavior is to reject unsupported return-type cleanup. The chosen behavior should be covered by a regression test using a routine with RETURNS the_table.
- **Why this is likely a bug:** The database result is specific and reproducible: target6 was absent after DROP TABLE target6 CASCADE, while unsupported6 was still present. This is not a browser or mock artifact; the SQL setup created a real table and function, and the source path independently explains the exact mismatch. A function's ReturnType is persisted as a separate field, but the PR's new cleanup and blocking checks inspect only parameter types, so a return-type dependency falls through to table deletion. Leaving a routine that references a deleted row type makes the catalog inconsistent and violates the test's dependency-safety contract. The targeted remediation is to inspect return types in the routine dependency handling and either drop those routines during CASCADE or reject the drop when that dependency is not supported.
**Relevant code:**
`server/hook/delete_table.go:50-67`
~~~go
if n.Cascade {
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}
for _, doltTable := range resolvedTables {
if err := beforeTableDeletionCheckTableColumns(ctx, doltTable, allTableNames); err != nil {
return nil, err
}
if err := beforeTableDeletionCheckFuncsProcs(ctx, doltTable, allTableNames); err != nil {
~~~
`server/hook/delete_table_cascade.go:257-279`
~~~go
func cascadeDropRoutines(ctx *sql.Context, allDeletedTables []doltdb.TableName) error {
deletedTypes := deletedTableTypes(allDeletedTables)
funcsColl, err := core.GetFunctionsCollectionFromContext(ctx, "")
...
err = funcsColl.IterateFunctions(ctx, func(f functions.Function) (stop bool, err error) {
for _, param := range f.AllParams {
if _, ok := deletedTypes[param.Type]; ok {
funcIDs = append(funcIDs, f.ID)
break
}
}
return false, nil
})
...
if err = funcsColl.DropFunction(ctx, funcIDs...); err != nil {
~~~
`core/functions/collection.go:40-54`
~~~go
type Function struct {
ID id.Function
ReturnType id.Type
AllParams []procedures.Parameter
Variadic bool
IsNonDeterministic bool
Strict bool
Definition string
ExtensionName string
ExtensionSymbol string
Operations []plpgsql.InterpreterOperation
SQLDefinition string
SetOf bool
}
~~~
`server/hook/delete_table.go:126-141`
~~~go
func beforeTableDeletionCheckFuncsProcs(ctx *sql.Context, doltTable *sqle.DoltTable, allDeletedTables []doltdb.TableName) error {
tableName := doltTable.TableName()
tableAsType := id.NewType(tableName.Schema, tableName.Name)
...
err = funcsColl.IterateFunctions(ctx, func(f functions.Function) (stop bool, err error) {
for _, param := range f.AllParams {
if param.Type == tableAsType {
return true, errors.Newf("cannot drop table %s because other objects depend on it ...")
~~~| TargetType: auth.AuthTargetType_TableIdentifiers, | ||
| TargetNames: authTableNames, | ||
| } | ||
| return &vitess.DDL{ |
There was a problem hiding this comment.
🆕 New Failure: identified in this diff run
Restrict deletes tables behind views
What failed: RESTRICT returned success and removed the base table. The dependent view remained in the catalog but could no longer be queried; the expected behavior is a dependency error with both objects preserved.
Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
- Severity: High
- Impact: A user who drops a table with RESTRICT can lose the table even when another view depends on it. The dependent view is left unusable, and restoring the table data may require a backup.
- Steps to Reproduce:
- Create a table named qa_drop1_base and insert one row.
- Create a view named qa_drop1_view that selects from qa_drop1_base.
- Run DROP TABLE qa_drop1_base RESTRICT.
- Check that the base table is gone and then query qa_drop1_view.
- Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
- Code Analysis: The parser conversion in server/ast/drop_table.go:45-53 now always returns a vitess DDL statement and sets Cascade to true only for tree.DropCascade. Therefore an explicit RESTRICT clause produces a standard plan.DropTable with Cascade=false. The new server/hook/delete_table.go:33-68 pre-execution hook calls cascadeDropDependencies only inside the n.Cascade branch at lines 50-55, then performs only the existing table-row-type and function/procedure-parameter checks at lines 56-67. It does not inspect dependent views on the non-cascade path. View discovery and dependency handling are implemented in server/hook/delete_table_cascade.go:80-128, but that code is reachable only through cascadeDropDependencies and therefore only for CASCADE. The local SQL evidence matches this path: DROP TABLE qa_drop1_base RESTRICT returned DROP TABLE with return code 0, the base relation was absent, and qa_drop1_view remained present but unusable. The smallest practical fix is to add dependent-view rejection to the non-CASCADE validation path, or restore the standard dependency check for views before allowing the table deletion; do not run cascadeDropViews for RESTRICT because RESTRICT must reject rather than remove the dependent view.
- Why this is likely a bug: PostgreSQL RESTRICT is expected to refuse a table drop when another relation depends on that table. The recorded local SQL result demonstrates the harmful state transition: the base table disappears, while the dependent view is left behind and cannot be queried. This is not explained by the unavailable browser service because the database command completed successfully and the source path explains exactly why view validation is skipped. The PR itself introduced the routing and hook condition that create the gap; a targeted non-cascade dependent-view check is the smallest practical remediation.
Relevant code
server/ast/drop_table.go:45-53
return &vitess.DDL{
Action: vitess.DropStr,
FromTables: tableNames,
IfExists: node.IfExists,
Cascade: node.DropBehavior == tree.DropCascade,
Auth: authInformation,
}, nilserver/hook/delete_table.go:50-68
if n.Cascade {
// CASCADE drops the objects that depend on the dropped tables before the standard drop path runs.
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}
// These checks error on any remaining dependency: everything with CASCADE was dropped above, so anything left
// (or anything at all, without CASCADE) blocks the drop.
for _, doltTable := range resolvedTables {server/hook/delete_table_cascade.go:80-84
func cascadeDropViews(ctx *sql.Context, runner sql.StatementRunner, allDeletedTables []doltdb.TableName) error {
views, viewExists, err := loadDatabaseViews(ctx)
if err != nil || len(views) == 0 {
return err
}Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.
**High severity — Restrict deletes tables behind views**
**What failed:** RESTRICT returned success and removed the base table. The dependent view remained in the catalog but could no longer be queried; the expected behavior is a dependency error with both objects preserved.
- **Impact:** A user who drops a table with RESTRICT can lose the table even when another view depends on it. The dependent view is left unusable, and restoring the table data may require a backup.
- **Steps to reproduce:**
1. Create a table named qa_drop1_base and insert one row.
2. Create a view named qa_drop1_view that selects from qa_drop1_base.
3. Run DROP TABLE qa_drop1_base RESTRICT.
4. Check that the base table is gone and then query qa_drop1_view.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The parser conversion in server/ast/drop_table.go:45-53 now always returns a vitess DDL statement and sets Cascade to true only for tree.DropCascade. Therefore an explicit RESTRICT clause produces a standard plan.DropTable with Cascade=false. The new server/hook/delete_table.go:33-68 pre-execution hook calls cascadeDropDependencies only inside the n.Cascade branch at lines 50-55, then performs only the existing table-row-type and function/procedure-parameter checks at lines 56-67. It does not inspect dependent views on the non-cascade path. View discovery and dependency handling are implemented in server/hook/delete_table_cascade.go:80-128, but that code is reachable only through cascadeDropDependencies and therefore only for CASCADE. The local SQL evidence matches this path: DROP TABLE qa_drop1_base RESTRICT returned DROP TABLE with return code 0, the base relation was absent, and qa_drop1_view remained present but unusable. The smallest practical fix is to add dependent-view rejection to the non-CASCADE validation path, or restore the standard dependency check for views before allowing the table deletion; do not run cascadeDropViews for RESTRICT because RESTRICT must reject rather than remove the dependent view.
- **Why this is likely a bug:** PostgreSQL RESTRICT is expected to refuse a table drop when another relation depends on that table. The recorded local SQL result demonstrates the harmful state transition: the base table disappears, while the dependent view is left behind and cannot be queried. This is not explained by the unavailable browser service because the database command completed successfully and the source path explains exactly why view validation is skipped. The PR itself introduced the routing and hook condition that create the gap; a targeted non-cascade dependent-view check is the smallest practical remediation.
**Relevant code:**
`server/ast/drop_table.go:45-53`
~~~go
return &vitess.DDL{
Action: vitess.DropStr,
FromTables: tableNames,
IfExists: node.IfExists,
Cascade: node.DropBehavior == tree.DropCascade,
Auth: authInformation,
}, nil
~~~
`server/hook/delete_table.go:50-68`
~~~go
if n.Cascade {
// CASCADE drops the objects that depend on the dropped tables before the standard drop path runs.
if err := cascadeDropDependencies(ctx, runner, allTableNames); err != nil {
return nil, err
}
}
// These checks error on any remaining dependency: everything with CASCADE was dropped above, so anything left
// (or anything at all, without CASCADE) blocks the drop.
for _, doltTable := range resolvedTables {
~~~
`server/hook/delete_table_cascade.go:80-84`
~~~go
func cascadeDropViews(ctx *sql.Context, runner sql.StatementRunner, allDeletedTables []doltdb.TableName) error {
views, viewExists, err := loadDatabaseViews(ctx)
if err != nil || len(views) == 0 {
return err
}
~~~
Hydrocharged
left a comment
There was a problem hiding this comment.
LGTM! This approach of using hooks is within the original intention, so that anything that resolves to a table deletion will run through this code.
| TargetType: auth.AuthTargetType_TableIdentifiers, | ||
| TargetNames: authTableNames, | ||
| } | ||
| return &vitess.DDL{ |
There was a problem hiding this comment.
Interesting that this is saying foreign keys aren't being dropped


Reworks DROP TABLE ... CASCADE to reuse the standard DROP TABLE plan node, with the pre-execution hook dropping dependent objects (views, foreign keys, row-type columns, and functions/procedures) at execution time.
Fixes #3120.
Companion PRs: