Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions server/analyzer/convert_drop_primary_key_constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2025 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 analyzer

import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/analyzer"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
)

// convertDropPrimaryKeyConstraint converts a DropConstraint node dropping a primary key constraint into
// an AlterPK node that GMS can process to remove the primary key.
func convertDropPrimaryKeyConstraint(ctx *sql.Context, _ *analyzer.Analyzer, n sql.Node, _ *plan.Scope, _ analyzer.RuleSelector, _ *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return transform.Node(n, func(n sql.Node) (sql.Node, transform.TreeIdentity, error) {
dropConstraint, ok := n.(*plan.DropConstraint)
if !ok {
return n, transform.SameTree, nil
}

rt, ok := dropConstraint.Child.(*plan.ResolvedTable)
if !ok {
return nil, transform.SameTree, analyzer.ErrInAnalysis.New(
"Expected a TableNode for ALTER TABLE DROP CONSTRAINT statement")
}

table := rt.Table
if it, ok := table.(sql.IndexAddressableTable); ok {
indexes, err := it.GetIndexes(ctx)
if err != nil {
return nil, transform.SameTree, err
}
for _, index := range indexes {
if index.ID() == "PRIMARY" && dropConstraint.Name == rt.Name()+"_pkey" {
return plan.NewAlterDropPk(rt.Database(), rt), transform.NewTree, nil
}
}
}

return n, transform.SameTree, nil
})
}
102 changes: 102 additions & 0 deletions server/analyzer/generate_fk_name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2025 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 analyzer

import (
"fmt"
"strings"

"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/analyzer"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
)

// generateForeignKeyName populates a generated foreign key name, in the Postgres default foreign key name format,
// when a foreign key is created without an explicit name specified.
func generateForeignKeyName(_ *sql.Context, _ *analyzer.Analyzer, n sql.Node, _ *plan.Scope, _ analyzer.RuleSelector, _ *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return transform.Node(n, func(n sql.Node) (sql.Node, transform.TreeIdentity, error) {
switch n := n.(type) {
case *plan.CreateTable:
copiedForeignKeys := make([]*sql.ForeignKeyConstraint, len(n.ForeignKeys()))
for i := range n.ForeignKeys() {
fk := *n.ForeignKeys()[i]
copiedForeignKeys[i] = &fk
}

changedForeignKey := false
for _, fk := range copiedForeignKeys {
if fk.Name == "" {
generatedName, err := generateFkName(n.Name(), fk, nil)
if err != nil {
return nil, transform.SameTree, err
}
changedForeignKey = true
fk.Name = generatedName
}
}
if changedForeignKey {
newCreateTable := plan.NewCreateTable(n.Db, n.Name(), n.IfNotExists(), n.Temporary(), &plan.TableSpec{
Schema: n.PkSchema(),
FkDefs: copiedForeignKeys,
ChDefs: n.Checks(),
IdxDefs: n.Indexes(),
Collation: n.Collation,
TableOpts: n.TableOpts,
})
return newCreateTable, transform.NewTree, nil
} else {
return n, transform.SameTree, nil
}

case *plan.CreateForeignKey:
if n.FkDef.Name == "" {
copiedFk := *n.FkDef
generatedName, err := generateFkName(copiedFk.Table, &copiedFk, nil)
if err != nil {
return nil, transform.SameTree, err
}
copiedFk.Name = generatedName
return &plan.CreateForeignKey{
DbProvider: n.DbProvider,
FkDef: &copiedFk,
}, transform.NewTree, nil
} else {
return n, transform.SameTree, nil
}

default:
return n, transform.SameTree, nil
}
})
}

// generateFkName creates a default foreign key name, according to Postgres naming rules (i.e. "<tablename>_<col1name>_<col2name>_fkey").
// |existingFks| is used to check that the generated name doesn't conflict with an existing foreign key name. If a
// conflicting name is generated, this function returns an error.
func generateFkName(tableName string, newFk *sql.ForeignKeyConstraint, existingFks []sql.ForeignKeyConstraint) (string, error) {
columnNames := strings.Join(newFk.Columns, "_")
generatedFkName := fmt.Sprintf("%s_%s_fkey", tableName, columnNames)

for _, existingFk := range existingFks {
if existingFk.Name == generatedFkName {
// TODO: Instead of returning an error, we should follow Postgres' behavior for disambiguating the name.
return "", fmt.Errorf("unable to create foreign key %s: "+
"a foreign key constraint already exists with this name", generatedFkName)
}
}

return generatedFkName, nil
}
41 changes: 23 additions & 18 deletions server/analyzer/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,32 @@ import (
// Comments are to match the Stringer formatting rules in the original rule definition file, but we can't generate
// human-readable strings for these extended types because they are in another package.
const (
ruleId_TypeSanitizer analyzer.RuleId = iota + 1000 // typeSanitizer
ruleId_AddDomainConstraints // addDomainConstraints
ruleId_AddDomainConstraintsToCasts // addDomainConstraintsToCasts
ruleId_AssignInsertCasts // assignInsertCasts
ruleId_AssignUpdateCasts // assignUpdateCasts
ruleId_ReplaceIndexedTables // replaceIndexedTables
ruleId_ReplaceNode // replaceNode
ruleId_ReplaceSerial // replaceSerial
ruleId_AddImplicitPrefixLengths // addImplicitPrefixLengths
ruleId_InsertContextRootFinalizer // insertContextRootFinalizer
ruleId_ResolveType // resolveType
ruleId_ReplaceArithmeticExpressions // replaceArithmeticExpressions
ruleId_OptimizeFunctions // optimizeFunctions
ruleId_ValidateColumnDefaults // validateColumnDefaults
ruleId_ValidateCreateTable // validateCreateTable
rulesId_ResolveAlterColumn // resolveAlterColumn
ruleId_TypeSanitizer analyzer.RuleId = iota + 1000 // typeSanitizer
ruleId_AddDomainConstraints // addDomainConstraints
ruleId_AddDomainConstraintsToCasts // addDomainConstraintsToCasts
ruleId_AssignInsertCasts // assignInsertCasts
ruleId_AssignUpdateCasts // assignUpdateCasts
ruleId_ConvertDropPrimaryKeyConstraint // convertDropPrimaryKeyConstraint
ruleId_GenerateForeignKeyName // generateForeignKeyName
ruleId_ReplaceIndexedTables // replaceIndexedTables
ruleId_ReplaceNode // replaceNode
ruleId_ReplaceSerial // replaceSerial
ruleId_AddImplicitPrefixLengths // addImplicitPrefixLengths
ruleId_InsertContextRootFinalizer // insertContextRootFinalizer
ruleId_ResolveType // resolveType
ruleId_ReplaceArithmeticExpressions // replaceArithmeticExpressions
ruleId_OptimizeFunctions // optimizeFunctions
ruleId_ValidateColumnDefaults // validateColumnDefaults
ruleId_ValidateCreateTable // validateCreateTable
ruleId_ResolveAlterColumn // resolveAlterColumn
)

// Init adds additional rules to the analyzer to handle Doltgres-specific functionality.
func Init() {
analyzer.AlwaysBeforeDefault = append(analyzer.AlwaysBeforeDefault,
analyzer.Rule{Id: ruleId_ResolveType, Apply: ResolveType},
analyzer.Rule{Id: ruleId_TypeSanitizer, Apply: TypeSanitizer},
analyzer.Rule{Id: ruleId_GenerateForeignKeyName, Apply: generateForeignKeyName},
analyzer.Rule{Id: ruleId_AddDomainConstraints, Apply: AddDomainConstraints},
analyzer.Rule{Id: ruleId_ValidateColumnDefaults, Apply: ValidateColumnDefaults},
analyzer.Rule{Id: ruleId_AssignInsertCasts, Apply: AssignInsertCasts},
Expand All @@ -54,14 +57,16 @@ func Init() {

// PostgreSQL doesn't have the concept of prefix lengths, so we add a rule to implicitly add them
// TODO: this should be replaced by implementing automatic toast semantics for blob types
analyzer.OnceBeforeDefault = append([]analyzer.Rule{{Id: ruleId_AddImplicitPrefixLengths, Apply: AddImplicitPrefixLengths}},
analyzer.OnceBeforeDefault = append([]analyzer.Rule{
{Id: ruleId_AddImplicitPrefixLengths, Apply: AddImplicitPrefixLengths},
{Id: ruleId_ConvertDropPrimaryKeyConstraint, Apply: convertDropPrimaryKeyConstraint}},
analyzer.OnceBeforeDefault...)

// We remove several validation rules and substitute our own
analyzer.OnceBeforeDefault = insertAnalyzerRules(analyzer.OnceBeforeDefault, analyzer.ValidateCreateTableId, true,
analyzer.Rule{Id: ruleId_ValidateCreateTable, Apply: validateCreateTable})
analyzer.OnceBeforeDefault = insertAnalyzerRules(analyzer.OnceBeforeDefault, analyzer.ResolveAlterColumnId, true,
analyzer.Rule{Id: rulesId_ResolveAlterColumn, Apply: resolveAlterColumn})
analyzer.Rule{Id: ruleId_ResolveAlterColumn, Apply: resolveAlterColumn})

analyzer.OnceBeforeDefault = removeAnalyzerRules(
analyzer.OnceBeforeDefault,
Expand Down
29 changes: 29 additions & 0 deletions server/ast/table_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ func assignTableDef(ctx *Context, node tree.TableDef, target *vitess.DDL) error
return err
}
target.TableSpec.AddColumn(columnDef)
if node.References.Table != nil {
fkDef, err := nodeForeignKeyDefinitionFromColumnTableDef(ctx, node.Name, node)
if err != nil {
return err
}
target.TableSpec.Constraints = append(target.TableSpec.Constraints, &vitess.ConstraintDefinition{
Details: fkDef,
})
}
return nil
case *tree.ForeignKeyConstraintTableDef:
if target.TableSpec == nil {
Expand Down Expand Up @@ -122,6 +131,26 @@ func assignTableDef(ctx *Context, node tree.TableDef, target *vitess.DDL) error
}
}

// nodeForeignKeyDefinitionFromColumnTableDef returns a vitess ForeignKeyDefinition from the specified column
// definition |node|.
func nodeForeignKeyDefinitionFromColumnTableDef(ctx *Context, fromColumn tree.Name, node *tree.ColumnTableDef) (*vitess.ForeignKeyDefinition, error) {
if node == nil {
return nil, nil
}

references := node.References
fkConstraintTableDef := &tree.ForeignKeyConstraintTableDef{
Name: references.ConstraintName,
FromCols: []tree.Name{fromColumn},
Table: *references.Table,
ToCols: []tree.Name{references.Col},
Actions: references.Actions,
Match: references.Match,
}

return nodeForeignKeyConstraintTableDef(ctx, fkConstraintTableDef)
}

// assignTableDefs handles tree.TableDefs nodes for *vitess.DDL targets. This also sorts table defs by whether they're
// dependent on other table defs evaluating first. Some table defs, such as indexes, affect other defs, such as columns,
// and they're therefore dependent on columns being handled first.
Expand Down
4 changes: 3 additions & 1 deletion server/tables/pgcatalog/pg_constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ func (p PgConstraintHandler) RowIter(ctx *sql.Context) (sql.RowIter, error) {
if index.Item.IsUnique() {
conType = "u"
} else {
conType = "f"
// If this isn't a primary key or a unique index, then it's a regular index, and not
// a constraint, so we don't need to report it in the pg_constraint table.
return true, nil
}
}

Expand Down
28 changes: 28 additions & 0 deletions testing/go/create_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/types"
)

func TestCreateTable(t *testing.T) {
Expand All @@ -43,6 +44,33 @@ func TestCreateTable(t *testing.T) {
{1, "Doe", "John"},
},
},
{
// Test that the PK constraint shows up in the information schema
Query: "SELECT conname FROM pg_constraint WHERE conrelid = 'employees'::regclass AND contype = 'p';",
Expected: []sql.Row{{"employees_pkey"}},
},
{
Query: "ALTER TABLE employees DROP CONSTRAINT employees_pkey;",
Expected: []sql.Row{},
},
},
},
{
// TODO: We don't currently support storing a custom name for a primary key constraint.
Skip: true,
Name: "create table with primary key, using custom constraint name",
SetUpScript: []string{
"CREATE TABLE users (id SERIAL, name TEXT, CONSTRAINT users_primary_key PRIMARY KEY (id));",
},
Assertions: []ScriptTestAssertion{
{
Query: "SELECT conname FROM pg_constraint WHERE conrelid = 'users'::regclass AND contype = 'p';",
Expected: []sql.Row{{"users_primary_key"}},
},
{
Query: "ALTER TABLE users DROP CONSTRAINT users_primary_key;",
Expected: []sql.Row{{types.NewOkResult(0)}},
},
},
},
{
Expand Down
Loading
Loading