From 94bfd08b0b4af4c7ee12f9dc72caee852ab0be09 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Wed, 19 Aug 2026 21:32:37 +0000 Subject: [PATCH 1/8] Support DROP TABLE ... CASCADE and RESTRICT 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). --- server/ast/drop_table.go | 50 ++++-- server/node/drop_table_cascade.go | 246 +++++++++++++++++++++++++++++ testing/go/drop_table_test.go | 247 ++++++++++++++++++++++++++++++ 3 files changed, 529 insertions(+), 14 deletions(-) create mode 100644 server/node/drop_table_cascade.go diff --git a/server/ast/drop_table.go b/server/ast/drop_table.go index 94d8754b5b..38619c1e33 100644 --- a/server/ast/drop_table.go +++ b/server/ast/drop_table.go @@ -16,26 +16,20 @@ package ast import ( "github.com/cockroachdb/errors" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/dolthub/doltgresql/postgres/parser/sem/tree" "github.com/dolthub/doltgresql/server/auth" + pgnodes "github.com/dolthub/doltgresql/server/node" ) // nodeDropTable handles *tree.DropTable nodes. -func nodeDropTable(ctx *Context, node *tree.DropTable) (*vitess.DDL, error) { +func nodeDropTable(ctx *Context, node *tree.DropTable) (vitess.Statement, error) { if node == nil || len(node.Names) == 0 { return nil, nil } - switch node.DropBehavior { - case tree.DropDefault: - // Default behavior, nothing to do - case tree.DropRestrict: - return nil, errors.Errorf("RESTRICT is not yet supported") - case tree.DropCascade: - return nil, errors.Errorf("CASCADE is not yet supported") - } tableNames := make([]vitess.TableName, len(node.Names)) authTableNames := make([]string, 0, len(node.Names)*3) for i := range node.Names { @@ -47,14 +41,42 @@ func nodeDropTable(ctx *Context, node *tree.DropTable) (*vitess.DDL, error) { authTableNames = append(authTableNames, tableNames[i].DbQualifier.String(), tableNames[i].SchemaQualifier.String(), tableNames[i].Name.String()) } + authInformation := vitess.AuthInformation{ + AuthType: auth.AuthType_DROPTABLE, + TargetType: auth.AuthTargetType_TableIdentifiers, + TargetNames: authTableNames, + } + switch node.DropBehavior { + case tree.DropDefault, tree.DropRestrict: + // RESTRICT is the default behavior in Postgres, so both are handled by the standard DROP TABLE path, which + // refuses to drop a table that other objects depend on. + case tree.DropCascade: + // CASCADE also drops the objects that depend on the dropped tables, which requires Doltgres-specific handling. + var databaseName string + dropTables := make([]doltdb.TableName, len(tableNames)) + for i, tableName := range tableNames { + dbQualifier := tableName.DbQualifier.String() + if len(dbQualifier) > 0 { + if len(databaseName) > 0 && databaseName != dbQualifier { + return nil, errors.Errorf("DROP TABLE CASCADE is currently only supported for a single database") + } + databaseName = dbQualifier + } + dropTables[i] = doltdb.TableName{ + Schema: tableName.SchemaQualifier.String(), + Name: tableName.Name.String(), + } + } + return vitess.InjectedStatement{ + Statement: pgnodes.NewDropTableCascade(node.IfExists, databaseName, dropTables), + Children: nil, + Auth: authInformation, + }, nil + } return &vitess.DDL{ Action: vitess.DropStr, FromTables: tableNames, IfExists: node.IfExists, - Auth: vitess.AuthInformation{ - AuthType: auth.AuthType_DROPTABLE, - TargetType: auth.AuthTargetType_TableIdentifiers, - TargetNames: authTableNames, - }, + Auth: authInformation, }, nil } diff --git a/server/node/drop_table_cascade.go b/server/node/drop_table_cascade.go new file mode 100644 index 0000000000..3731a533a4 --- /dev/null +++ b/server/node/drop_table_cascade.go @@ -0,0 +1,246 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package node + +import ( + "context" + "fmt" + "strings" + + "github.com/cockroachdb/errors" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/plan" + vitess "github.com/dolthub/vitess/go/vt/sqlparser" + + "github.com/dolthub/doltgresql/core" +) + +// DropTableCascade handles the DROP TABLE ... CASCADE statement. It first drops any objects that the dependency +// tracking knows depend on the dropped tables (currently foreign key constraints on other tables that reference the +// dropped tables), then delegates the actual table drop to the standard DROP TABLE path. Sequences owned by the +// dropped tables' columns (e.g. SERIAL columns) are dropped by the standard path itself. +type DropTableCascade struct { + // database is the database qualifier given in the statement, if any. Only the current database is supported. + database string + // tables are the (possibly schema-qualified) names of the tables to drop. + tables []doltdb.TableName + ifExists bool +} + +var _ sql.ExecSourceRel = (*DropTableCascade)(nil) +var _ vitess.Injectable = (*DropTableCascade)(nil) + +// NewDropTableCascade returns a new *DropTableCascade. +func NewDropTableCascade(ifExists bool, database string, tables []doltdb.TableName) *DropTableCascade { + return &DropTableCascade{ + database: database, + tables: tables, + ifExists: ifExists, + } +} + +// Children implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) Children() []sql.Node { + return nil +} + +// IsReadOnly implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) IsReadOnly() bool { + return false +} + +// Resolved implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) Resolved() bool { + return true +} + +// RowIter implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) { + if len(c.database) > 0 && c.database != ctx.GetCurrentDatabase() { + return nil, errors.Errorf("DROP TABLE CASCADE is currently only supported for the current database") + } + runner, err := core.GetRunnerFromContext(ctx) + if err != nil { + return nil, err + } + if runner == nil { + return nil, errors.Errorf("DROP TABLE CASCADE requires a statement runner, but one was not found in the context") + } + _, root, err := core.GetRootFromContext(ctx) + if err != nil { + return nil, err + } + + // Resolve each of the given table names. Names without an explicit schema are resolved against the search path, + // matching the behavior of the regular DROP TABLE path. + var dropTables []doltdb.TableName + for _, tblName := range c.tables { + resolvedName, found, err := c.resolveTable(ctx, root, tblName) + if err != nil { + return nil, err + } + if !found { + if c.ifExists { + // TODO: issue a notice that the table is being skipped + continue + } + return nil, errors.Errorf(`table "%s" does not exist`, tblName.Name) + } + dropTables = append(dropTables, resolvedName) + } + if len(dropTables) == 0 { + return sql.RowsToRowIter(), nil + } + inDropSet := func(name doltdb.TableName) bool { + for _, dropped := range dropTables { + if dropped.EqualFold(name) { + return true + } + } + return false + } + + // Drop the foreign keys on other tables that reference the tables being dropped. These constraints depend on the + // dropped tables, so CASCADE removes them. Foreign keys declared by tables that are themselves being dropped + // (including self-referential keys) are removed along with their tables by the standard DROP TABLE path. This uses + // the same interface calls that the engine's DROP TABLE execution uses for a dropped table's declared keys. + fkc, err := root.GetForeignKeyCollection(ctx) + if err != nil { + return nil, err + } + for _, tblName := range dropTables { + _, referencedByFk := fkc.KeysForTable(tblName) + if len(referencedByFk) == 0 { + continue + } + sqlTable, err := core.GetSqlTableFromContext(ctx, "", tblName) + if err != nil { + return nil, err + } + if sqlTable == nil { + return nil, errors.Errorf(`table "%s" was resolved but could not be found`, tblName.Name) + } + fkTable, ok := sqlTable.(sql.ForeignKeyTable) + if !ok { + continue + } + for _, fk := range referencedByFk { + if inDropSet(fk.TableName) { + continue + } + // TODO: issue a notice that the constraint is being dropped ("drop cascades to constraint ... on table ...") + if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil { + return nil, err + } + } + } + + // Drop the tables themselves by running the equivalent DROP TABLE statement. This reuses the standard path, + // including its own dependency bookkeeping (such as dropping sequences owned by the tables' columns). + _, err = sql.RunInterpreted(ctx, func(subCtx *sql.Context) (struct{}, error) { + quotedNames := make([]string, len(dropTables)) + for i, tblName := range dropTables { + quotedNames[i] = quotedQualifiedName(tblName) + } + dropTable := fmt.Sprintf(`DROP TABLE %s;`, strings.Join(quotedNames, ", ")) + return struct{}{}, runStatement(subCtx, runner, dropTable) + }) + if err != nil { + return nil, err + } + return sql.RowsToRowIter(), nil +} + +// resolveTable resolves the given table name to a schema-qualified name, returning whether the table was found. Names +// without an explicit schema are resolved against the search path. Temporary tables (which do not live in the root) +// are returned as-is. +func (c *DropTableCascade) resolveTable(ctx *sql.Context, root *core.RootValue, tblName doltdb.TableName) (doltdb.TableName, bool, error) { + if len(tblName.Schema) == 0 { + resolvedName, found, err := resolve.TableName(ctx, root, tblName.Name) + if err != nil { + return doltdb.TableName{}, false, err + } + if found { + return resolvedName, true, nil + } + } else { + found, err := root.HasTable(ctx, tblName) + if err != nil { + return doltdb.TableName{}, false, err + } + if found { + return tblName, true, nil + } + } + // The table may be a temporary table, which does not live in the root. Temporary tables cannot be referenced by + // foreign keys on permanent tables, so there are no dependent constraints to find for them. + relationType, err := core.GetRelationType(ctx, tblName.Schema, tblName.Name) + if err != nil { + return doltdb.TableName{}, false, err + } + if relationType == core.RelationType_Table { + return tblName, true, nil + } + return doltdb.TableName{}, false, nil +} + +// Schema implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) Schema(ctx *sql.Context) sql.Schema { + return nil +} + +// String implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) String() string { + return "DROP TABLE CASCADE" +} + +// WithChildren implements the interface sql.ExecSourceRel. +func (c *DropTableCascade) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) { + return plan.NillaryWithChildren(c, children...) +} + +// WithResolvedChildren implements the interface vitess.Injectable. +func (c *DropTableCascade) WithResolvedChildren(ctx context.Context, children []any) (any, error) { + if len(children) != 0 { + return nil, ErrVitessChildCount.New(0, len(children)) + } + return c, nil +} + +// runStatement runs the given statement on the given runner, draining and discarding any returned rows. +func runStatement(ctx *sql.Context, runner sql.StatementRunner, statement string) error { + _, rowIter, _, err := runner.QueryWithBindings(ctx, statement, nil, nil, nil) + if err != nil { + return err + } + _, err = sql.RowIterToRows(ctx, rowIter) + return err +} + +// quoteIdentifier returns the given identifier in its quoted form. +func quoteIdentifier(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} + +// quotedQualifiedName returns the given table name in its quoted, schema-qualified form. Names without a schema are +// returned unqualified. +func quotedQualifiedName(name doltdb.TableName) string { + if len(name.Schema) == 0 { + return quoteIdentifier(name.Name) + } + return quoteIdentifier(name.Schema) + "." + quoteIdentifier(name.Name) +} diff --git a/testing/go/drop_table_test.go b/testing/go/drop_table_test.go index 57e44e9120..f083d7e399 100644 --- a/testing/go/drop_table_test.go +++ b/testing/go/drop_table_test.go @@ -107,3 +107,250 @@ func TestDropTable(t *testing.T) { }, }) } + +func TestDropTableCascade(t *testing.T) { + RunScripts(t, []ScriptTest{ + { + Name: "DROP TABLE CASCADE drops foreign keys referencing the table", + SetUpScript: []string{ + `CREATE TABLE parent (pk INT4 PRIMARY KEY, v1 TEXT);`, + `CREATE TABLE child (pk INT4 PRIMARY KEY, parent_pk INT4, CONSTRAINT child_parent_fk FOREIGN KEY (parent_pk) REFERENCES parent (pk));`, + `INSERT INTO parent VALUES (1, 'one');`, + `INSERT INTO child VALUES (10, 1);`, + }, + Assertions: []ScriptTestAssertion{ + { + // Without CASCADE, dropping a table that a foreign key references is an error + Query: `DROP TABLE parent;`, + ExpectedErr: "cannot drop table", + }, + { + // RESTRICT is the default behavior, so it errors the same way + Query: `DROP TABLE parent RESTRICT;`, + ExpectedErr: "cannot drop table", + }, + { + Query: `DROP TABLE parent CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM parent;`, + ExpectedErr: "not found", + }, + { + // The child table sticks around, only its foreign key constraint was dropped + Query: `SELECT * FROM child;`, + Expected: []sql.Row{{10, 1}}, + }, + { + // The foreign key constraint no longer exists, so this insert succeeds + Query: `INSERT INTO child VALUES (11, 99);`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM child ORDER BY pk;`, + Expected: []sql.Row{{10, 1}, {11, 99}}, + }, + }, + }, + { + Name: "DROP TABLE RESTRICT succeeds when nothing depends on the table", + SetUpScript: []string{ + `CREATE TABLE test (pk INT4 PRIMARY KEY);`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE test RESTRICT;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM test;`, + ExpectedErr: "not found", + }, + }, + }, + { + Name: "DROP TABLE CASCADE with no dependencies", + SetUpScript: []string{ + `CREATE TABLE test (pk INT4 PRIMARY KEY);`, + `INSERT INTO test VALUES (1);`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM test;`, + ExpectedErr: "not found", + }, + }, + }, + { + Name: "DROP TABLE CASCADE with a self-referential foreign key", + SetUpScript: []string{ + `CREATE TABLE test (pk INT4 PRIMARY KEY, parent_pk INT4, CONSTRAINT test_self_fk FOREIGN KEY (parent_pk) REFERENCES test (pk));`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM test;`, + ExpectedErr: "not found", + }, + }, + }, + { + Name: "DROP TABLE CASCADE with multiple tables", + SetUpScript: []string{ + `CREATE TABLE parent (pk INT4 PRIMARY KEY);`, + `CREATE TABLE middle (pk INT4 PRIMARY KEY, parent_pk INT4, CONSTRAINT middle_parent_fk FOREIGN KEY (parent_pk) REFERENCES parent (pk));`, + `CREATE TABLE child (pk INT4 PRIMARY KEY, middle_pk INT4, CONSTRAINT child_middle_fk FOREIGN KEY (middle_pk) REFERENCES middle (pk));`, + }, + Assertions: []ScriptTestAssertion{ + { + // parent is listed before middle, which references it; middle's foreign key on parent is handled + // by dropping both tables, while child's foreign key on middle is dropped by the cascade + Query: `DROP TABLE parent, middle CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM parent;`, + ExpectedErr: "not found", + }, + { + Query: `SELECT * FROM middle;`, + ExpectedErr: "not found", + }, + { + Query: `INSERT INTO child VALUES (1, 99);`, + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "DROP TABLE IF EXISTS CASCADE", + SetUpScript: []string{ + `CREATE TABLE parent (pk INT4 PRIMARY KEY);`, + `CREATE TABLE child (pk INT4 PRIMARY KEY, parent_pk INT4, CONSTRAINT child_parent_fk FOREIGN KEY (parent_pk) REFERENCES parent (pk));`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE IF EXISTS doesnotexist CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `DROP TABLE doesnotexist CASCADE;`, + ExpectedErr: `table "doesnotexist" does not exist`, + }, + { + Query: `DROP TABLE IF EXISTS doesnotexist, parent CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM parent;`, + ExpectedErr: "not found", + }, + { + Query: `INSERT INTO child VALUES (1, 99);`, + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "DROP TABLE CASCADE with schema-qualified names", + SetUpScript: []string{ + `CREATE SCHEMA sch1;`, + `CREATE SCHEMA sch2;`, + `CREATE TABLE sch1.parent (pk INT4 PRIMARY KEY);`, + `CREATE TABLE sch2.child (pk INT4 PRIMARY KEY, parent_pk INT4, CONSTRAINT child_parent_fk FOREIGN KEY (parent_pk) REFERENCES sch1.parent (pk));`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE sch1.parent;`, + ExpectedErr: "cannot drop table", + }, + { + Query: `DROP TABLE sch1.parent CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM sch1.parent;`, + ExpectedErr: "not found", + }, + { + Query: `INSERT INTO sch2.child VALUES (1, 99);`, + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "DROP TABLE CASCADE resolves tables on the search path", + SetUpScript: []string{ + `CREATE SCHEMA sch1;`, + `SET search_path TO sch1;`, + `CREATE TABLE parent (pk INT4 PRIMARY KEY);`, + `CREATE TABLE child (pk INT4 PRIMARY KEY, parent_pk INT4, CONSTRAINT child_parent_fk FOREIGN KEY (parent_pk) REFERENCES parent (pk));`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE parent CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM sch1.parent;`, + ExpectedErr: "not found", + }, + { + Query: `INSERT INTO sch1.child VALUES (1, 99);`, + Expected: []sql.Row{}, + }, + }, + }, + { + Name: "DROP TABLE CASCADE drops sequences owned by the table", + SetUpScript: []string{ + `CREATE TABLE test (pk SERIAL PRIMARY KEY, v1 TEXT);`, + `INSERT INTO test (v1) VALUES ('one');`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT nextval('test_pk_seq');`, + Expected: []sql.Row{{2}}, + }, + { + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + // The sequence backing the SERIAL column was dropped along with the table + Query: `SELECT nextval('test_pk_seq');`, + ExpectedErr: "does not exist", + }, + }, + }, + { + Name: "DROP TABLE CASCADE with a dependent view", + SetUpScript: []string{ + `CREATE TABLE test (pk INT4 PRIMARY KEY, v1 TEXT);`, + `INSERT INTO test VALUES (1, 'one');`, + `CREATE VIEW test_view AS SELECT * FROM test;`, + }, + Assertions: []ScriptTestAssertion{ + { + // TODO: view dependencies on tables are not yet tracked, so CASCADE cannot drop the dependent + // view; the table drop succeeds and leaves the view behind (which errors when used), matching + // the behavior of DROP TABLE without CASCADE + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM test_view;`, + ExpectedErr: "references invalid table", + }, + }, + }, + }) +} From b9fcf2c4446336ec2038834b6f60eedec50f13b0 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Mon, 31 Aug 2026 14:19:43 -0700 Subject: [PATCH 2/8] fixed test --- .../command_docs/output/drop_table_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testing/generation/command_docs/output/drop_table_test.go b/testing/generation/command_docs/output/drop_table_test.go index cdccad18c2..d7dc5407f4 100644 --- a/testing/generation/command_docs/output/drop_table_test.go +++ b/testing/generation/command_docs/output/drop_table_test.go @@ -22,14 +22,14 @@ func TestDropTable(t *testing.T) { Converts("DROP TABLE IF EXISTS name"), Converts("DROP TABLE name , name"), Converts("DROP TABLE IF EXISTS name , name"), - Parses("DROP TABLE name CASCADE"), - Parses("DROP TABLE IF EXISTS name CASCADE"), - Parses("DROP TABLE name , name CASCADE"), - Parses("DROP TABLE IF EXISTS name , name CASCADE"), - Parses("DROP TABLE name RESTRICT"), - Parses("DROP TABLE IF EXISTS name RESTRICT"), - Parses("DROP TABLE name , name RESTRICT"), - Parses("DROP TABLE IF EXISTS name , name RESTRICT"), + Converts("DROP TABLE name CASCADE"), + Converts("DROP TABLE IF EXISTS name CASCADE"), + Converts("DROP TABLE name , name CASCADE"), + Converts("DROP TABLE IF EXISTS name , name CASCADE"), + Converts("DROP TABLE name RESTRICT"), + Converts("DROP TABLE IF EXISTS name RESTRICT"), + Converts("DROP TABLE name , name RESTRICT"), + Converts("DROP TABLE IF EXISTS name , name RESTRICT"), } RunTests(t, tests) } From 094f5b14031e3db0fb40977c223453a4753ad710 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Mon, 31 Aug 2026 22:07:56 +0000 Subject: [PATCH 3/8] Rework DROP TABLE CASCADE to reuse the standard DROP TABLE node 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. --- server/ast/drop_table.go | 37 +- server/connection_handler.go | 2 +- server/hook/delete_table.go | 11 +- server/hook/delete_table_cascade.go | 540 ++++++++++++++++++++++++++++ server/node/drop_table_cascade.go | 246 ------------- testing/go/drop_table_test.go | 121 ++++++- 6 files changed, 671 insertions(+), 286 deletions(-) create mode 100644 server/hook/delete_table_cascade.go delete mode 100644 server/node/drop_table_cascade.go diff --git a/server/ast/drop_table.go b/server/ast/drop_table.go index 38619c1e33..2278bc6364 100644 --- a/server/ast/drop_table.go +++ b/server/ast/drop_table.go @@ -15,14 +15,10 @@ package ast import ( - "github.com/cockroachdb/errors" - "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" - vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/dolthub/doltgresql/postgres/parser/sem/tree" "github.com/dolthub/doltgresql/server/auth" - pgnodes "github.com/dolthub/doltgresql/server/node" ) // nodeDropTable handles *tree.DropTable nodes. @@ -46,37 +42,14 @@ func nodeDropTable(ctx *Context, node *tree.DropTable) (vitess.Statement, error) TargetType: auth.AuthTargetType_TableIdentifiers, TargetNames: authTableNames, } - switch node.DropBehavior { - case tree.DropDefault, tree.DropRestrict: - // RESTRICT is the default behavior in Postgres, so both are handled by the standard DROP TABLE path, which - // refuses to drop a table that other objects depend on. - case tree.DropCascade: - // CASCADE also drops the objects that depend on the dropped tables, which requires Doltgres-specific handling. - var databaseName string - dropTables := make([]doltdb.TableName, len(tableNames)) - for i, tableName := range tableNames { - dbQualifier := tableName.DbQualifier.String() - if len(dbQualifier) > 0 { - if len(databaseName) > 0 && databaseName != dbQualifier { - return nil, errors.Errorf("DROP TABLE CASCADE is currently only supported for a single database") - } - databaseName = dbQualifier - } - dropTables[i] = doltdb.TableName{ - Schema: tableName.SchemaQualifier.String(), - Name: tableName.Name.String(), - } - } - return vitess.InjectedStatement{ - Statement: pgnodes.NewDropTableCascade(node.IfExists, databaseName, dropTables), - Children: nil, - Auth: authInformation, - }, nil - } return &vitess.DDL{ Action: vitess.DropStr, FromTables: tableNames, IfExists: node.IfExists, - Auth: authInformation, + // RESTRICT is the default behavior in Postgres, so both are handled by the standard DROP TABLE path, which + // refuses to drop a table that other objects depend on. CASCADE also drops the dependent objects, which the + // DROP TABLE pre-execution hook handles. + Cascade: node.DropBehavior == tree.DropCascade, + Auth: authInformation, }, nil } diff --git a/server/connection_handler.go b/server/connection_handler.go index 8337535b23..ecc0aa5731 100644 --- a/server/connection_handler.go +++ b/server/connection_handler.go @@ -1600,7 +1600,7 @@ func castSQLError(err error) *pgconn.PgError { code = pgcode.ForeignKeyViolation case sql.ErrCheckConstraintViolated.Is(err), pgtypes.ErrDomainValueViolatesCheckConstraint.Is(err): code = pgcode.CheckViolation - case sql.ErrInsertIntoNonNullableProvidedNull.Is(err), sql.ErrInsertIntoNonNullableDefaultNullColumn.Is(err), + case sql.ErrInsertIntoNonNullableProvidedNull.Is(err), sql.ErrColumnDefaultReturnedNull.Is(err), pgtypes.ErrDomainDoesNotAllowNullValues.Is(err): code = pgcode.NotNullViolation // Class 25 — Invalid Transaction State diff --git a/server/hook/delete_table.go b/server/hook/delete_table.go index 59011c4c94..66dc602855 100644 --- a/server/hook/delete_table.go +++ b/server/hook/delete_table.go @@ -30,7 +30,7 @@ import ( // BeforeTableDeletion performs all validation necessary to ensure that table deletion does not leave the database in an // invalid state. -func BeforeTableDeletion(ctx *sql.Context, _ sql.StatementRunner, nodeInterface sql.Node) (sql.Node, error) { +func BeforeTableDeletion(ctx *sql.Context, runner sql.StatementRunner, nodeInterface sql.Node) (sql.Node, error) { // TODO: handle casts using a table name n, ok := nodeInterface.(*plan.DropTable) if !ok { @@ -47,7 +47,14 @@ func BeforeTableDeletion(ctx *sql.Context, _ sql.StatementRunner, nodeInterface resolvedTables = append(resolvedTables, doltTable) allTableNames = append(allTableNames, doltTable.TableName()) } - // TODO: handle DROP TABLE CASCADE + 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 { // Check if the table is in a column if err := beforeTableDeletionCheckTableColumns(ctx, doltTable, allTableNames); err != nil { diff --git a/server/hook/delete_table_cascade.go b/server/hook/delete_table_cascade.go new file mode 100644 index 0000000000..8df17e2018 --- /dev/null +++ b/server/hook/delete_table_cascade.go @@ -0,0 +1,540 @@ +// Copyright 2026 Dolthub, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hook + +import ( + "fmt" + "strings" + + "github.com/cockroachdb/errors" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/core" + "github.com/dolthub/doltgresql/core/functions" + "github.com/dolthub/doltgresql/core/id" + "github.com/dolthub/doltgresql/core/procedures" + "github.com/dolthub/doltgresql/postgres/parser/parser" + "github.com/dolthub/doltgresql/postgres/parser/sem/tree" + "github.com/dolthub/doltgresql/server/settings" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// cascadeDropDependencies drops the objects that depend on the tables being dropped, implementing the CASCADE drop +// behavior. Views whose definitions reference the dropped tables (directly, or through other dependent views) are +// dropped, as are foreign keys on other tables that reference the dropped tables, functions and procedures with a +// parameter of a dropped table's row type, and columns of other tables whose type is a dropped table's row type. +// Sequences owned by the dropped tables' columns (e.g. SERIAL columns) are dropped by the standard DROP TABLE path +// itself. Foreign keys declared by tables that are themselves being dropped (including self-referential keys) are +// also removed by the standard path. +func cascadeDropDependencies(ctx *sql.Context, runner sql.StatementRunner, allDeletedTables []doltdb.TableName) error { + if err := cascadeDropViews(ctx, runner, allDeletedTables); err != nil { + return err + } + if err := cascadeDropForeignKeys(ctx, allDeletedTables); err != nil { + return err + } + if err := cascadeDropRoutines(ctx, allDeletedTables); err != nil { + return err + } + return cascadeDropDependentColumns(ctx, runner, allDeletedTables) +} + +// relationKey identifies a relation (table or view) by its lowercased schema and name. +type relationKey struct { + schema string + name string +} + +// newRelationKey returns the relationKey for the given schema and name. +func newRelationKey(schema string, name string) relationKey { + return relationKey{schema: strings.ToLower(schema), name: strings.ToLower(name)} +} + +// cascadeView is a view in the current database, along with the table names its definition references. +type cascadeView struct { + schema string + name string + refs []*tree.TableName + dependent bool +} + +// cascadeDropViews drops the views whose definitions reference the tables being dropped. A view depends on a dropped +// table if its definition references the table directly, or references another view that does. Dependencies are +// determined by parsing each view's definition and resolving the referenced names the same way the engine would when +// the view is queried: explicitly qualified names match as given, and unqualified names resolve against the search +// path. Column-level dependencies are not tracked, so a view is only dropped when a dropped relation appears in its +// definition by name. +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 + } + _, root, err := core.GetRootFromContext(ctx) + if err != nil { + return err + } + searchPath, err := settings.GetCurrentSchemas(ctx) + if err != nil { + return err + } + + // Compute the closure of dependent relations: dropping a view may make further views dependent, so iterate until + // no new views are added. + closure := make(map[relationKey]struct{}, len(allDeletedTables)) + for _, tblName := range allDeletedTables { + closure[newRelationKey(tblName.Schema, tblName.Name)] = struct{}{} + } + for changed := true; changed; { + changed = false + for _, view := range views { + if view.dependent { + continue + } + dependent, err := viewDependsOnClosure(ctx, root, view, closure, searchPath, viewExists) + if err != nil { + return err + } + if dependent { + view.dependent = true + closure[newRelationKey(view.schema, view.name)] = struct{}{} + changed = true + } + } + } + + for _, view := range views { + if !view.dependent { + continue + } + // TODO: issue a notice that the view is being dropped ("drop cascades to view ...") + dropStmt := fmt.Sprintf(`DROP VIEW %s.%s;`, quoteIdentifier(view.schema), quoteIdentifier(view.name)) + if err = runStatement(ctx, runner, dropStmt); err != nil { + return err + } + } + return nil +} + +// loadDatabaseViews returns all views in the current database with their parsed table references, along with a set of +// the views' relation keys for name resolution. +func loadDatabaseViews(ctx *sql.Context) ([]*cascadeView, map[relationKey]struct{}, error) { + db, err := core.GetSqlDatabaseFromContext(ctx, "") + if err != nil { + return nil, nil, err + } + schemaDb, ok := db.(sql.SchemaDatabase) + if !ok { + return nil, nil, nil + } + schemas, err := schemaDb.AllSchemas(ctx) + if err != nil { + return nil, nil, err + } + var views []*cascadeView + viewExists := make(map[relationKey]struct{}) + for _, schema := range schemas { + viewDb, ok := schema.(sql.ViewDatabase) + if !ok { + continue + } + defs, err := viewDb.AllViews(ctx) + if err != nil { + return nil, nil, err + } + for _, def := range defs { + stmts, err := parser.Parse(def.CreateViewStatement) + if err != nil || len(stmts) == 0 { + return nil, nil, errors.Newf("could not parse the definition of view %s.%s: %v", + schema.SchemaName(), def.Name, err) + } + createView, ok := stmts[0].AST.(*tree.CreateView) + if !ok { + // Definitions that aren't plain CREATE VIEW statements (e.g. materialized views) don't have their + // dependencies tracked yet, so they are left in place. + continue + } + collector := newTableRefCollector() + collector.collectSelect(createView.AsSource) + views = append(views, &cascadeView{ + schema: schema.SchemaName(), + name: def.Name, + refs: collector.refs, + }) + viewExists[newRelationKey(schema.SchemaName(), def.Name)] = struct{}{} + } + } + return views, viewExists, nil +} + +// viewDependsOnClosure returns whether any of the given view's referenced names resolve to a relation in the given +// closure. Unqualified names resolve to the first schema on the search path containing a relation with that name, +// mirroring how the engine resolves the name when the view is queried. +func viewDependsOnClosure(ctx *sql.Context, root *core.RootValue, view *cascadeView, closure map[relationKey]struct{}, + searchPath []string, viewExists map[relationKey]struct{}) (bool, error) { + for _, ref := range view.refs { + if ref.ExplicitSchema { + if _, ok := closure[newRelationKey(ref.Schema(), ref.Table())]; ok { + return true, nil + } + continue + } + for _, schema := range searchPath { + key := newRelationKey(schema, ref.Table()) + if _, ok := closure[key]; ok { + return true, nil + } + // If the name resolves to a relation that isn't being dropped, then it shadows any same-named relation + // later on the search path. + if _, ok := viewExists[key]; ok { + break + } + hasTable, err := root.HasTable(ctx, doltdb.TableName{Schema: schema, Name: ref.Table()}) + if err != nil { + return false, err + } + if hasTable { + break + } + } + } + return false, nil +} + +// cascadeDropForeignKeys drops the foreign keys on other tables that reference the tables being dropped. These +// constraints depend on the dropped tables, so CASCADE removes them. This uses the same interface calls that the +// engine's DROP TABLE execution uses for a dropped table's declared keys. +func cascadeDropForeignKeys(ctx *sql.Context, allDeletedTables []doltdb.TableName) error { + _, root, err := core.GetRootFromContext(ctx) + if err != nil { + return err + } + fkc, err := root.GetForeignKeyCollection(ctx) + if err != nil { + return err + } + for _, tblName := range allDeletedTables { + _, referencedByFk := fkc.KeysForTable(tblName) + if len(referencedByFk) == 0 { + continue + } + sqlTable, err := core.GetSqlTableFromContext(ctx, "", tblName) + if err != nil { + return err + } + if sqlTable == nil { + return errors.Newf(`table "%s" was resolved but could not be found`, tblName.Name) + } + fkTable, ok := sqlTable.(sql.ForeignKeyTable) + if !ok { + continue + } + for _, fk := range referencedByFk { + if tableNameInSet(fk.TableName, allDeletedTables) { + continue + } + // TODO: issue a notice that the constraint is being dropped ("drop cascades to constraint ... on table ...") + if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil { + return err + } + } + } + return nil +} + +// cascadeDropRoutines drops the functions and procedures that have a parameter typed with a dropped table's row type. +func cascadeDropRoutines(ctx *sql.Context, allDeletedTables []doltdb.TableName) error { + deletedTypes := deletedTableTypes(allDeletedTables) + funcsColl, err := core.GetFunctionsCollectionFromContext(ctx, "") + if err != nil { + return err + } + var funcIDs []id.Function + 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 != nil { + return err + } + // TODO: issue a notice for each dropped routine ("drop cascades to function ...") + if err = funcsColl.DropFunction(ctx, funcIDs...); err != nil { + return err + } + procsColl, err := core.GetProceduresCollectionFromContext(ctx, "") + if err != nil { + return err + } + var procIDs []id.Procedure + err = procsColl.IterateProcedures(ctx, func(p procedures.Procedure) (stop bool, err error) { + for _, param := range p.AllParams { + if _, ok := deletedTypes[param.Type]; ok { + procIDs = append(procIDs, p.ID) + break + } + } + return false, nil + }) + if err != nil { + return err + } + return procsColl.DropProcedure(ctx, procIDs...) +} + +// cascadeDropDependentColumns drops the columns of other tables whose type is a dropped table's row type. This matches +// Postgres, which drops just the dependent column rather than the whole table. +func cascadeDropDependentColumns(ctx *sql.Context, runner sql.StatementRunner, allDeletedTables []doltdb.TableName) error { + _, root, err := core.GetRootFromContext(ctx) + if err != nil { + return err + } + deletedTypes := deletedTableTypes(allDeletedTables) + allTableNames, err := root.GetAllTableNames(ctx, false) + if err != nil { + return err + } + type dependentColumn struct { + table doltdb.TableName + column string + } + // Collect all the dependent columns before dropping any, so that the scan works from a consistent root. + var dependentColumns []dependentColumn + for _, otherTableName := range allTableNames { + if doltdb.IsSystemTable(otherTableName) { + // System tables don't use any table types + continue + } + if tableNameInSet(otherTableName, allDeletedTables) { + // If we're also deleting this table, then it doesn't matter what the columns have + continue + } + otherTable, ok, err := root.GetTable(ctx, otherTableName) + if err != nil { + return err + } + if !ok { + return errors.Newf("root returned table name `%s` but it could not be found?", otherTableName.String()) + } + otherTableSch, err := otherTable.GetSchema(ctx) + if err != nil { + return err + } + for _, col := range otherTableSch.GetAllCols().GetColumns() { + dgtype, ok := col.TypeInfo.ToSqlType().(*pgtypes.DoltgresType) + if !ok { + // If this isn't a Doltgres type, then it can't be a table type so we can ignore it + continue + } + if _, ok = deletedTypes[dgtype.ID]; ok { + dependentColumns = append(dependentColumns, dependentColumn{table: otherTableName, column: col.Name}) + } + } + } + for _, depCol := range dependentColumns { + // TODO: issue a notice that the column is being dropped ("drop cascades to column ... of table ...") + alterStmt := fmt.Sprintf(`ALTER TABLE %s DROP COLUMN %s;`, + quotedQualifiedName(depCol.table), quoteIdentifier(depCol.column)) + if err = runStatement(ctx, runner, alterStmt); err != nil { + return err + } + } + return nil +} + +// deletedTableTypes returns the set of row types belonging to the given tables. +func deletedTableTypes(allDeletedTables []doltdb.TableName) map[id.Type]struct{} { + deletedTypes := make(map[id.Type]struct{}, len(allDeletedTables)) + for _, tblName := range allDeletedTables { + deletedTypes[id.NewType(tblName.Schema, tblName.Name)] = struct{}{} + } + return deletedTypes +} + +// tableNameInSet returns whether the given table name is in the given set of table names, ignoring case. +func tableNameInSet(name doltdb.TableName, set []doltdb.TableName) bool { + for _, candidate := range set { + if candidate.EqualFold(name) { + return true + } + } + return false +} + +// runStatement runs the given statement on the given runner, draining and discarding any returned rows. The statement +// runs as though it were interpreted, since it's a new statement running inside the original one. +func runStatement(ctx *sql.Context, runner sql.StatementRunner, statement string) error { + _, err := sql.RunInterpreted(ctx, func(subCtx *sql.Context) (struct{}, error) { + _, rowIter, _, err := runner.QueryWithBindings(subCtx, statement, nil, nil, nil) + if err != nil { + return struct{}{}, err + } + _, err = sql.RowIterToRows(subCtx, rowIter) + return struct{}{}, err + }) + return err +} + +// quoteIdentifier returns the given identifier in its quoted form. +func quoteIdentifier(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} + +// quotedQualifiedName returns the given table name in its quoted, schema-qualified form. Names without a schema are +// returned unqualified. +func quotedQualifiedName(name doltdb.TableName) string { + if len(name.Schema) == 0 { + return quoteIdentifier(name.Name) + } + return quoteIdentifier(name.Schema) + "." + quoteIdentifier(name.Name) +} + +// tableRefCollector collects the table names referenced by a view definition. Common table expression names are +// tracked so that references to them are not mistaken for table references. +type tableRefCollector struct { + refs []*tree.TableName + cteNames map[string]struct{} +} + +var _ tree.Visitor = (*tableRefCollector)(nil) + +// newTableRefCollector returns a new *tableRefCollector. +func newTableRefCollector() *tableRefCollector { + return &tableRefCollector{cteNames: make(map[string]struct{})} +} + +// VisitPre implements the interface tree.Visitor. It recurses into subqueries appearing in expression position. +func (c *tableRefCollector) VisitPre(expr tree.Expr) (recurse bool, newExpr tree.Expr) { + if subquery, ok := expr.(*tree.Subquery); ok { + c.collectSelectStatement(subquery.Select) + } + return true, expr +} + +// VisitPost implements the interface tree.Visitor. +func (c *tableRefCollector) VisitPost(expr tree.Expr) tree.Expr { + return expr +} + +// collectExpr collects the table references from any subqueries within the given expression. +func (c *tableRefCollector) collectExpr(expr tree.Expr) { + if expr != nil { + tree.WalkExpr(c, expr) + } +} + +// collectSelect collects the table references from the given SELECT statement, including its CTEs, ORDER BY, and +// LIMIT clauses. +func (c *tableRefCollector) collectSelect(sel *tree.Select) { + if sel == nil { + return + } + if sel.With != nil { + for _, cte := range sel.With.CTEList { + c.collectStatement(cte.Stmt) + c.cteNames[strings.ToLower(string(cte.Name.Alias))] = struct{}{} + } + } + c.collectSelectStatement(sel.Select) + for _, order := range sel.OrderBy { + c.collectExpr(order.Expr) + } + if sel.Limit != nil { + c.collectExpr(sel.Limit.Count) + c.collectExpr(sel.Limit.Offset) + } +} + +// collectSelectStatement collects the table references from the given select statement variant. +func (c *tableRefCollector) collectSelectStatement(stmt tree.SelectStatement) { + switch stmt := stmt.(type) { + case *tree.ParenSelect: + c.collectSelect(stmt.Select) + case *tree.SelectClause: + for _, tableExpr := range stmt.From.Tables { + c.collectTableExpr(tableExpr) + } + for i := range stmt.Exprs { + c.collectExpr(stmt.Exprs[i].Expr) + } + if stmt.Where != nil { + c.collectExpr(stmt.Where.Expr) + } + for _, expr := range stmt.GroupBy { + c.collectExpr(expr) + } + if stmt.Having != nil { + c.collectExpr(stmt.Having.Expr) + } + for _, expr := range stmt.DistinctOn { + c.collectExpr(expr) + } + case *tree.UnionClause: + c.collectSelect(stmt.Left) + c.collectSelect(stmt.Right) + case *tree.ValuesClause: + for _, row := range stmt.Rows { + for _, expr := range row { + c.collectExpr(expr) + } + } + } +} + +// collectStatement collects the table references from the given statement, which may be any statement form that can +// appear in a CTE. +func (c *tableRefCollector) collectStatement(stmt tree.Statement) { + switch stmt := stmt.(type) { + case *tree.Select: + c.collectSelect(stmt) + case tree.SelectStatement: + c.collectSelectStatement(stmt) + } +} + +// collectTableExpr collects the table references from the given table expression. +func (c *tableRefCollector) collectTableExpr(expr tree.TableExpr) { + switch expr := expr.(type) { + case *tree.AliasedTableExpr: + c.collectTableExpr(expr.Expr) + case *tree.ParenTableExpr: + c.collectTableExpr(expr.Expr) + case *tree.JoinTableExpr: + c.collectTableExpr(expr.Left) + c.collectTableExpr(expr.Right) + if cond, ok := expr.Cond.(*tree.OnJoinCond); ok { + c.collectExpr(cond.Expr) + } + case *tree.TableName: + if !expr.ExplicitSchema { + if _, ok := c.cteNames[strings.ToLower(expr.Table())]; ok { + return + } + } + c.refs = append(c.refs, expr) + case *tree.Subquery: + c.collectSelectStatement(expr.Select) + case *tree.StatementSource: + c.collectStatement(expr.Statement) + case *tree.RowsFromExpr: + for _, item := range expr.Items { + c.collectExpr(item) + } + } +} diff --git a/server/node/drop_table_cascade.go b/server/node/drop_table_cascade.go deleted file mode 100644 index 3731a533a4..0000000000 --- a/server/node/drop_table_cascade.go +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2026 Dolthub, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package node - -import ( - "context" - "fmt" - "strings" - - "github.com/cockroachdb/errors" - "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" - "github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve" - "github.com/dolthub/go-mysql-server/sql" - "github.com/dolthub/go-mysql-server/sql/plan" - vitess "github.com/dolthub/vitess/go/vt/sqlparser" - - "github.com/dolthub/doltgresql/core" -) - -// DropTableCascade handles the DROP TABLE ... CASCADE statement. It first drops any objects that the dependency -// tracking knows depend on the dropped tables (currently foreign key constraints on other tables that reference the -// dropped tables), then delegates the actual table drop to the standard DROP TABLE path. Sequences owned by the -// dropped tables' columns (e.g. SERIAL columns) are dropped by the standard path itself. -type DropTableCascade struct { - // database is the database qualifier given in the statement, if any. Only the current database is supported. - database string - // tables are the (possibly schema-qualified) names of the tables to drop. - tables []doltdb.TableName - ifExists bool -} - -var _ sql.ExecSourceRel = (*DropTableCascade)(nil) -var _ vitess.Injectable = (*DropTableCascade)(nil) - -// NewDropTableCascade returns a new *DropTableCascade. -func NewDropTableCascade(ifExists bool, database string, tables []doltdb.TableName) *DropTableCascade { - return &DropTableCascade{ - database: database, - tables: tables, - ifExists: ifExists, - } -} - -// Children implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) Children() []sql.Node { - return nil -} - -// IsReadOnly implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) IsReadOnly() bool { - return false -} - -// Resolved implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) Resolved() bool { - return true -} - -// RowIter implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) { - if len(c.database) > 0 && c.database != ctx.GetCurrentDatabase() { - return nil, errors.Errorf("DROP TABLE CASCADE is currently only supported for the current database") - } - runner, err := core.GetRunnerFromContext(ctx) - if err != nil { - return nil, err - } - if runner == nil { - return nil, errors.Errorf("DROP TABLE CASCADE requires a statement runner, but one was not found in the context") - } - _, root, err := core.GetRootFromContext(ctx) - if err != nil { - return nil, err - } - - // Resolve each of the given table names. Names without an explicit schema are resolved against the search path, - // matching the behavior of the regular DROP TABLE path. - var dropTables []doltdb.TableName - for _, tblName := range c.tables { - resolvedName, found, err := c.resolveTable(ctx, root, tblName) - if err != nil { - return nil, err - } - if !found { - if c.ifExists { - // TODO: issue a notice that the table is being skipped - continue - } - return nil, errors.Errorf(`table "%s" does not exist`, tblName.Name) - } - dropTables = append(dropTables, resolvedName) - } - if len(dropTables) == 0 { - return sql.RowsToRowIter(), nil - } - inDropSet := func(name doltdb.TableName) bool { - for _, dropped := range dropTables { - if dropped.EqualFold(name) { - return true - } - } - return false - } - - // Drop the foreign keys on other tables that reference the tables being dropped. These constraints depend on the - // dropped tables, so CASCADE removes them. Foreign keys declared by tables that are themselves being dropped - // (including self-referential keys) are removed along with their tables by the standard DROP TABLE path. This uses - // the same interface calls that the engine's DROP TABLE execution uses for a dropped table's declared keys. - fkc, err := root.GetForeignKeyCollection(ctx) - if err != nil { - return nil, err - } - for _, tblName := range dropTables { - _, referencedByFk := fkc.KeysForTable(tblName) - if len(referencedByFk) == 0 { - continue - } - sqlTable, err := core.GetSqlTableFromContext(ctx, "", tblName) - if err != nil { - return nil, err - } - if sqlTable == nil { - return nil, errors.Errorf(`table "%s" was resolved but could not be found`, tblName.Name) - } - fkTable, ok := sqlTable.(sql.ForeignKeyTable) - if !ok { - continue - } - for _, fk := range referencedByFk { - if inDropSet(fk.TableName) { - continue - } - // TODO: issue a notice that the constraint is being dropped ("drop cascades to constraint ... on table ...") - if err = fkTable.DropForeignKey(ctx, fk.Name, fk.TableName.Name, fk.TableName.Schema); err != nil { - return nil, err - } - } - } - - // Drop the tables themselves by running the equivalent DROP TABLE statement. This reuses the standard path, - // including its own dependency bookkeeping (such as dropping sequences owned by the tables' columns). - _, err = sql.RunInterpreted(ctx, func(subCtx *sql.Context) (struct{}, error) { - quotedNames := make([]string, len(dropTables)) - for i, tblName := range dropTables { - quotedNames[i] = quotedQualifiedName(tblName) - } - dropTable := fmt.Sprintf(`DROP TABLE %s;`, strings.Join(quotedNames, ", ")) - return struct{}{}, runStatement(subCtx, runner, dropTable) - }) - if err != nil { - return nil, err - } - return sql.RowsToRowIter(), nil -} - -// resolveTable resolves the given table name to a schema-qualified name, returning whether the table was found. Names -// without an explicit schema are resolved against the search path. Temporary tables (which do not live in the root) -// are returned as-is. -func (c *DropTableCascade) resolveTable(ctx *sql.Context, root *core.RootValue, tblName doltdb.TableName) (doltdb.TableName, bool, error) { - if len(tblName.Schema) == 0 { - resolvedName, found, err := resolve.TableName(ctx, root, tblName.Name) - if err != nil { - return doltdb.TableName{}, false, err - } - if found { - return resolvedName, true, nil - } - } else { - found, err := root.HasTable(ctx, tblName) - if err != nil { - return doltdb.TableName{}, false, err - } - if found { - return tblName, true, nil - } - } - // The table may be a temporary table, which does not live in the root. Temporary tables cannot be referenced by - // foreign keys on permanent tables, so there are no dependent constraints to find for them. - relationType, err := core.GetRelationType(ctx, tblName.Schema, tblName.Name) - if err != nil { - return doltdb.TableName{}, false, err - } - if relationType == core.RelationType_Table { - return tblName, true, nil - } - return doltdb.TableName{}, false, nil -} - -// Schema implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) Schema(ctx *sql.Context) sql.Schema { - return nil -} - -// String implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) String() string { - return "DROP TABLE CASCADE" -} - -// WithChildren implements the interface sql.ExecSourceRel. -func (c *DropTableCascade) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) { - return plan.NillaryWithChildren(c, children...) -} - -// WithResolvedChildren implements the interface vitess.Injectable. -func (c *DropTableCascade) WithResolvedChildren(ctx context.Context, children []any) (any, error) { - if len(children) != 0 { - return nil, ErrVitessChildCount.New(0, len(children)) - } - return c, nil -} - -// runStatement runs the given statement on the given runner, draining and discarding any returned rows. -func runStatement(ctx *sql.Context, runner sql.StatementRunner, statement string) error { - _, rowIter, _, err := runner.QueryWithBindings(ctx, statement, nil, nil, nil) - if err != nil { - return err - } - _, err = sql.RowIterToRows(ctx, rowIter) - return err -} - -// quoteIdentifier returns the given identifier in its quoted form. -func quoteIdentifier(name string) string { - return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` -} - -// quotedQualifiedName returns the given table name in its quoted, schema-qualified form. Names without a schema are -// returned unqualified. -func quotedQualifiedName(name doltdb.TableName) string { - if len(name.Schema) == 0 { - return quoteIdentifier(name.Name) - } - return quoteIdentifier(name.Schema) + "." + quoteIdentifier(name.Name) -} diff --git a/testing/go/drop_table_test.go b/testing/go/drop_table_test.go index f083d7e399..4959e022db 100644 --- a/testing/go/drop_table_test.go +++ b/testing/go/drop_table_test.go @@ -243,7 +243,8 @@ func TestDropTableCascade(t *testing.T) { }, { Query: `DROP TABLE doesnotexist CASCADE;`, - ExpectedErr: `table "doesnotexist" does not exist`, + // This matches the standard DROP TABLE path's error; Postgres says `table "doesnotexist" does not exist` + ExpectedErr: `table not found: doesnotexist`, }, { Query: `DROP TABLE IF EXISTS doesnotexist, parent CASCADE;`, @@ -340,15 +341,125 @@ func TestDropTableCascade(t *testing.T) { }, Assertions: []ScriptTestAssertion{ { - // TODO: view dependencies on tables are not yet tracked, so CASCADE cannot drop the dependent - // view; the table drop succeeds and leaves the view behind (which errors when used), matching - // the behavior of DROP TABLE without CASCADE Query: `DROP TABLE test CASCADE;`, Expected: []sql.Row{}, }, { Query: `SELECT * FROM test_view;`, - ExpectedErr: "references invalid table", + ExpectedErr: "not found", + }, + }, + }, + { + Name: "DROP TABLE CASCADE drops transitively dependent views only", + SetUpScript: []string{ + `CREATE TABLE test (pk INT4 PRIMARY KEY, v1 TEXT);`, + `CREATE TABLE other (pk INT4 PRIMARY KEY);`, + `INSERT INTO test VALUES (1, 'one');`, + `INSERT INTO other VALUES (7);`, + `CREATE VIEW test_view AS SELECT * FROM test;`, + `CREATE VIEW test_view_view AS SELECT pk FROM test_view;`, + `CREATE VIEW other_view AS SELECT * FROM other;`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM test_view;`, + ExpectedErr: "not found", + }, + { + // The view on the dependent view is dropped as well + Query: `SELECT * FROM test_view_view;`, + ExpectedErr: "not found", + }, + { + // Views that don't depend on the dropped table are left alone + Query: `SELECT * FROM other_view;`, + Expected: []sql.Row{{7}}, + }, + }, + }, + { + Name: "DROP TABLE CASCADE does not drop views resolving to a same-named table in another schema", + SetUpScript: []string{ + `CREATE SCHEMA sch1;`, + `CREATE TABLE test (pk INT4 PRIMARY KEY);`, + `CREATE TABLE sch1.test (pk INT4 PRIMARY KEY);`, + `INSERT INTO sch1.test VALUES (3);`, + `CREATE VIEW qualified_view AS SELECT * FROM sch1.test;`, + `CREATE VIEW unqualified_view AS SELECT * FROM test;`, + }, + Assertions: []ScriptTestAssertion{ + { + // Drops public.test; sch1.test and the views that resolve to it survive + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM qualified_view;`, + Expected: []sql.Row{{3}}, + }, + { + // The unqualified reference resolved to public.test, which was dropped + Query: `SELECT * FROM unqualified_view;`, + ExpectedErr: "not found", + }, + }, + }, + { + Name: "DROP TABLE CASCADE drops columns using the table's row type", + SetUpScript: []string{ + `CREATE TABLE test1 (pk INT4 PRIMARY KEY, v1 TEXT);`, + `CREATE TABLE test2 (pk INT4 PRIMARY KEY, v1 test1);`, + `INSERT INTO test2 VALUES (1, ROW(2, 'abc')::test1);`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE test1;`, + ExpectedErr: "cannot drop table test1 because other objects depend on it", + }, + { + // The dependent column is dropped, not the whole table + Query: `DROP TABLE test1 CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT * FROM test2;`, + Expected: []sql.Row{{1}}, + }, + }, + }, + { + Name: "DROP TABLE CASCADE drops functions and procedures using the table's row type", + SetUpScript: []string{ + `CREATE TABLE test (pk INT4 PRIMARY KEY, v1 TEXT);`, + `CREATE FUNCTION dependent_func(t test) RETURNS INT4 AS $$ BEGIN RETURN t.pk * 2; END; $$ LANGUAGE plpgsql;`, + `CREATE FUNCTION unrelated_func(v INT4) RETURNS INT4 AS $$ BEGIN RETURN v + 1; END; $$ LANGUAGE plpgsql;`, + `CREATE PROCEDURE dependent_proc(input test) AS $$ BEGIN END; $$ LANGUAGE plpgsql;`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `DROP TABLE test;`, + ExpectedErr: "cannot drop table test because other objects depend on it", + }, + { + Query: `DROP TABLE test CASCADE;`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT dependent_func(NULL);`, + ExpectedErr: "not found", + }, + { + Query: `SELECT unrelated_func(1);`, + Expected: []sql.Row{{2}}, + }, + { + Query: `CALL dependent_proc(NULL);`, + ExpectedErr: "does not exist", }, }, }, From ab5fd63f3527ddd8663996921426d469af2f4ff3 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Mon, 31 Aug 2026 22:28:19 +0000 Subject: [PATCH 4/8] Bump GMS and vitess for the DROP TABLE Cascade flag --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index bde462a469..ca2aa95840 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,10 @@ require ( github.com/dolthub/dolt/go v0.40.5-0.20260818221349-fd24ec0e2bea github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260817180248-8ba7438d98bb + github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081 github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 - github.com/dolthub/vitess v0.0.0-20260728212736-0542037326d7 + github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 github.com/fatih/color v1.13.0 github.com/go-sql-driver/mysql v1.9.3 github.com/goccy/go-json v0.10.2 diff --git a/go.sum b/go.sum index 46d55f12e0..a89295d9a5 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/dolthub/fslock v0.0.5 h1:QoXhBhgY1oumHE26qyE7tgmXUT8qjJwxsIzo54O/B/k= github.com/dolthub/fslock v0.0.5/go.mod h1:sdofYYqE0D79zNZyB4/kmlnsQOVap1C2yByjGKSirEM= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260817180248-8ba7438d98bb h1:PsTj02vQmCjGCBxT029GI5mihk5vncnVkiI9xpazlAc= -github.com/dolthub/go-mysql-server v0.20.1-0.20260817180248-8ba7438d98bb/go.mod h1:+kBhllUzhxourihWb4h69IzBx3+fg1mXrPKeI4JkS3w= +github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081 h1:B1WcPDul86sUwKSNjAzYViGc64IhjuCcyCHb7di3ZVo= +github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081/go.mod h1:W1u45n8fFmy/C4dYXoQ2jQPzbKTA8WX2kNjLkusig2I= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= @@ -268,8 +268,8 @@ github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 h1:GY17cGA4 github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1/go.mod h1:qnrZP3/1slFl2Bq5yw38HLOsArZareGwdpEceriblLc= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 h1:fdexJBpLDyAGnFbviZYfZ0jhLMnbTWTh39CyQ1EhA28= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166/go.mod h1:e/FIZVvT2IR53HBCAo41NjqgtEnjMJGKca3Y/dAmZaA= -github.com/dolthub/vitess v0.0.0-20260728212736-0542037326d7 h1:gvOpHisi0CbG3hixdGulNQdXy3V7+AWxe37kGBgTmRI= -github.com/dolthub/vitess v0.0.0-20260728212736-0542037326d7/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= +github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 h1:NMg7ETQiEHsSfH8ckCTI7q0ukycwOF4qEuJ/jO1QUIM= +github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= From 44463b2ada5f32df16aab3508e98b7a3c1eaf212 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Mon, 31 Aug 2026 15:35:56 -0700 Subject: [PATCH 5/8] new deps --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 99208147de..7bba248cc2 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,10 @@ require ( github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260831214236-aa1f8e45655b + github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081 github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 - github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659 + github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 github.com/fatih/color v1.13.0 github.com/go-sql-driver/mysql v1.9.3 github.com/goccy/go-json v0.10.2 diff --git a/go.sum b/go.sum index 1b048697d6..5162d00ce4 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/dolthub/fslock v0.0.5 h1:QoXhBhgY1oumHE26qyE7tgmXUT8qjJwxsIzo54O/B/k= github.com/dolthub/fslock v0.0.5/go.mod h1:sdofYYqE0D79zNZyB4/kmlnsQOVap1C2yByjGKSirEM= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260831214236-aa1f8e45655b h1:O9lohpiOrRXsdogCmnxLT2P7ox4u+Ilat6pl6b8f2+w= -github.com/dolthub/go-mysql-server v0.20.1-0.20260831214236-aa1f8e45655b/go.mod h1:CtDdfkAma4klv+UQbEmV4AkVvkSyXf/V3iRpIDu6c2I= +github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081 h1:B1WcPDul86sUwKSNjAzYViGc64IhjuCcyCHb7di3ZVo= +github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081/go.mod h1:W1u45n8fFmy/C4dYXoQ2jQPzbKTA8WX2kNjLkusig2I= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= @@ -268,8 +268,8 @@ github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 h1:GY17cGA4 github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1/go.mod h1:qnrZP3/1slFl2Bq5yw38HLOsArZareGwdpEceriblLc= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 h1:fdexJBpLDyAGnFbviZYfZ0jhLMnbTWTh39CyQ1EhA28= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166/go.mod h1:e/FIZVvT2IR53HBCAo41NjqgtEnjMJGKca3Y/dAmZaA= -github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659 h1:DJfoBlihBdLY6KokA+wCXoYYeOgwrKuiZFPHJP8koi8= -github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= +github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 h1:NMg7ETQiEHsSfH8ckCTI7q0ukycwOF4qEuJ/jO1QUIM= +github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= From d09facdac483c522800b12f0ce6290575051c7c2 Mon Sep 17 00:00:00 2001 From: zachmu Date: Mon, 31 Aug 2026 22:43:25 +0000 Subject: [PATCH 6/8] [ga-format-pr] Run scripts/format_repo.sh --- testing/go/drop_table_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/go/drop_table_test.go b/testing/go/drop_table_test.go index 4959e022db..0b5db65a48 100644 --- a/testing/go/drop_table_test.go +++ b/testing/go/drop_table_test.go @@ -242,7 +242,7 @@ func TestDropTableCascade(t *testing.T) { Expected: []sql.Row{}, }, { - Query: `DROP TABLE doesnotexist CASCADE;`, + Query: `DROP TABLE doesnotexist CASCADE;`, // This matches the standard DROP TABLE path's error; Postgres says `table "doesnotexist" does not exist` ExpectedErr: `table not found: doesnotexist`, }, From 24ca166a853efde7af20a0c18ab58d823f6201c6 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Tue, 1 Sep 2026 12:38:19 -0700 Subject: [PATCH 7/8] new deps --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7bba248cc2..39b9ac8985 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/cockroachdb/apd/v3 v3.2.3 github.com/cockroachdb/errors v1.7.5 - github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0 + github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081 + github.com/dolthub/go-mysql-server v0.20.1-0.20260901193414-1d66394566bb github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 diff --git a/go.sum b/go.sum index 5162d00ce4..f62b37fce8 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,8 @@ github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12 h1:I github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12/go.mod h1:rN7X8BHwkjPcfMQQ2QTAq/xM3leUSGLfb+1Js7Y6TVo= github.com/dolthub/dolt-mcp v0.3.4 h1:AyG5cw+fNWXDHXujtQnqUPZrpWtPg6FN6yYtjv1pP44= github.com/dolthub/dolt-mcp v0.3.4/go.mod h1:bCZ7KHvDYs+M0e+ySgmGiNvLhcwsN7bbf5YCyillLrk= -github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0 h1:VrCgqBaqm3WT7olZRdYGScPGsZdzd4S67M/hD/R00f8= -github.com/dolthub/dolt/go v0.40.5-0.20260828173935-dc4e79209ce0/go.mod h1:vGmWnRXHjHe+BYLeaC2WCte5lpzEXdcKFt4mFiKD2yc= +github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917 h1:lx7XhihYV9y0H5N6US13VlN5JGjT7FB9eksXXA7sMCU= +github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917/go.mod h1:D1MXT5V70cZfYR8eKpijs/1vsOFgSGdmr9hk83gi73Y= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 h1:0mg9QEFdkkBwJMxvz1tCjHYmfG2iIC6aShj1InDq9/M= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4/go.mod h1:SSLraQS/jGLYFgff3vuZ+JbVUct6vyEeMzjLBqWqoyM= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= @@ -256,8 +256,8 @@ github.com/dolthub/fslock v0.0.5 h1:QoXhBhgY1oumHE26qyE7tgmXUT8qjJwxsIzo54O/B/k= github.com/dolthub/fslock v0.0.5/go.mod h1:sdofYYqE0D79zNZyB4/kmlnsQOVap1C2yByjGKSirEM= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081 h1:B1WcPDul86sUwKSNjAzYViGc64IhjuCcyCHb7di3ZVo= -github.com/dolthub/go-mysql-server v0.20.1-0.20260831222604-8a70b0008081/go.mod h1:W1u45n8fFmy/C4dYXoQ2jQPzbKTA8WX2kNjLkusig2I= +github.com/dolthub/go-mysql-server v0.20.1-0.20260901193414-1d66394566bb h1:aWfAutMf3Y+g5rFzTMvZoMeN2gy0hZbSiaQdVLbQHMQ= +github.com/dolthub/go-mysql-server v0.20.1-0.20260901193414-1d66394566bb/go.mod h1:W1u45n8fFmy/C4dYXoQ2jQPzbKTA8WX2kNjLkusig2I= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= From cc477fb93e9ac2a645eaedaa4ece61c789fa6958 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Tue, 1 Sep 2026 13:24:42 -0700 Subject: [PATCH 8/8] new gms --- go.mod | 5 +++-- go.sum | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 28808e6b56..2ef6f2bd90 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,10 @@ require ( github.com/dolthub/dolt/go v0.40.5-0.20260901102237-645f6accd917 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260901192735-5d6be3976cd5 + github.com/dolthub/go-mysql-server v0.20.1-0.20260901200741-de2dfefb8dec github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 - github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659 + github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 github.com/fatih/color v1.13.0 github.com/go-sql-driver/mysql v1.9.3 github.com/goccy/go-json v0.10.2 @@ -142,6 +142,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d // indirect github.com/kch42/buzhash v0.0.0-20160816060738-9bdec3dec7c6 // indirect github.com/klauspost/compress v1.18.0 // indirect diff --git a/go.sum b/go.sum index 3dfa8324f7..28a9170558 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/dolthub/fslock v0.0.5 h1:QoXhBhgY1oumHE26qyE7tgmXUT8qjJwxsIzo54O/B/k= github.com/dolthub/fslock v0.0.5/go.mod h1:sdofYYqE0D79zNZyB4/kmlnsQOVap1C2yByjGKSirEM= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroDnY/BXyAffVZxUpXhP2GpoUJyyq5KaLn8c= github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260901192735-5d6be3976cd5 h1:OjIEbos9AZW660TkKANOzDjMnOZgFchyJkhcXXulCcE= -github.com/dolthub/go-mysql-server v0.20.1-0.20260901192735-5d6be3976cd5/go.mod h1:CtDdfkAma4klv+UQbEmV4AkVvkSyXf/V3iRpIDu6c2I= +github.com/dolthub/go-mysql-server v0.20.1-0.20260901200741-de2dfefb8dec h1:kwUmkRRUt7HD9umb/+mM5OTCnA9LYznwJBqyMpF6dMM= +github.com/dolthub/go-mysql-server v0.20.1-0.20260901200741-de2dfefb8dec/go.mod h1:W1u45n8fFmy/C4dYXoQ2jQPzbKTA8WX2kNjLkusig2I= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= @@ -268,8 +268,8 @@ github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 h1:GY17cGA4 github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1/go.mod h1:qnrZP3/1slFl2Bq5yw38HLOsArZareGwdpEceriblLc= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 h1:fdexJBpLDyAGnFbviZYfZ0jhLMnbTWTh39CyQ1EhA28= github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166/go.mod h1:e/FIZVvT2IR53HBCAo41NjqgtEnjMJGKca3Y/dAmZaA= -github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659 h1:DJfoBlihBdLY6KokA+wCXoYYeOgwrKuiZFPHJP8koi8= -github.com/dolthub/vitess v0.0.0-20260828193927-f9eb707fd659/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= +github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960 h1:NMg7ETQiEHsSfH8ckCTI7q0ukycwOF4qEuJ/jO1QUIM= +github.com/dolthub/vitess v0.0.0-20260831192502-e34df639d960/go.mod h1:5SVEJgAhw5nnQUFnGgKI1Svqes1Mw+zKUsalyxmOuG0= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -520,7 +520,6 @@ github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jcmturner/gofork v0.0.0-20180107083740-2aebee971930/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=