chore: import upstream snapshot with attribution
This commit is contained in:
Executable
+462
@@ -0,0 +1,462 @@
|
||||
// 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
// validateCreateTable validates that a table can be created as specified
|
||||
func validateCreateTable(ctx *sql.Context, a *analyzer.Analyzer, n sql.Node, scope *plan.Scope, sel analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
|
||||
ct, ok := n.(*plan.CreateTable)
|
||||
if !ok {
|
||||
return n, transform.SameTree, nil
|
||||
}
|
||||
|
||||
err := validateIdentifiers(ct)
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
sch := ct.PkSchema().Schema
|
||||
idxs := ct.Indexes()
|
||||
err = validateIndexes(ctx, sch, idxs)
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
return n, transform.SameTree, nil
|
||||
}
|
||||
|
||||
// validateIdentifiers validates the names of all schema elements for validity
|
||||
// TODO: we use 64 character as the max length for an identifier, postgres uses 63
|
||||
func validateIdentifiers(ct *plan.CreateTable) error {
|
||||
err := analyzer.ValidateIdentifier(ct.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
colNames := make(map[string]bool)
|
||||
for _, col := range ct.PkSchema().Schema {
|
||||
err = analyzer.ValidateIdentifier(col.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lower := strings.ToLower(col.Name)
|
||||
if colNames[lower] {
|
||||
return sql.ErrDuplicateColumn.New(col.Name)
|
||||
}
|
||||
colNames[lower] = true
|
||||
}
|
||||
|
||||
for _, chDef := range ct.Checks() {
|
||||
err = analyzer.ValidateIdentifier(chDef.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, idxDef := range ct.Indexes() {
|
||||
err = analyzer.ValidateIdentifier(idxDef.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, fkDef := range ct.ForeignKeys() {
|
||||
err = analyzer.ValidateIdentifier(fkDef.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateIndexes validates that the index definitions being created are valid
|
||||
func validateIndexes(ctx *sql.Context, sch sql.Schema, idxDefs sql.IndexDefs) error {
|
||||
colMap := schToColMap(sch)
|
||||
for _, idxDef := range idxDefs {
|
||||
if err := validateIndex(ctx, colMap, idxDef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// schToColMap returns a map of columns, keyed by their name, for the specified
|
||||
// schema |sch|.
|
||||
func schToColMap(sch sql.Schema) map[string]*sql.Column {
|
||||
colMap := make(map[string]*sql.Column, len(sch))
|
||||
for _, col := range sch {
|
||||
colMap[strings.ToLower(col.Name)] = col
|
||||
}
|
||||
return colMap
|
||||
}
|
||||
|
||||
// validateIndex ensures that the Index Definition is valid for the table schema.
|
||||
// This function will throw errors and warnings as needed.
|
||||
// All columns in the index must be:
|
||||
// - in the schema
|
||||
// - not duplicated
|
||||
// - a compatible type for an index
|
||||
//
|
||||
// TODO: there are other constraints on indexes that we could enforce and are not yet (e.g. JSON as an index)
|
||||
func validateIndex(ctx *sql.Context, colMap map[string]*sql.Column, idxDef *sql.IndexDef) error {
|
||||
seenCols := make(map[string]struct{})
|
||||
for _, idxCol := range idxDef.Columns {
|
||||
if idxCol.Expression != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
schCol, exists := colMap[strings.ToLower(idxCol.Name)]
|
||||
if !exists {
|
||||
return sql.ErrKeyColumnDoesNotExist.New(idxCol.Name)
|
||||
}
|
||||
if _, ok := seenCols[schCol.Name]; ok {
|
||||
return sql.ErrDuplicateColumn.New(schCol.Name)
|
||||
}
|
||||
seenCols[schCol.Name] = struct{}{}
|
||||
if idxDef.IsFullText() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if idxDef.IsSpatial() {
|
||||
return errors.Errorf("spatial indexes are not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveAlterColumn is a validation rule that validates the schema changes in an ALTER TABLE statement and updates
|
||||
// the nodes with necessary intermediate / update schema information
|
||||
func resolveAlterColumn(ctx *sql.Context, a *analyzer.Analyzer, n sql.Node, scope *plan.Scope, sel analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
|
||||
if !analyzer.FlagIsSet(qFlags, sql.QFlagAlterTable) {
|
||||
return n, transform.SameTree, nil
|
||||
}
|
||||
|
||||
var sch sql.Schema
|
||||
var indexes []string
|
||||
var validator sql.SchemaValidator
|
||||
keyedColumns := make(map[string]bool)
|
||||
var err error
|
||||
transform.Inspect(n, func(n sql.Node) bool {
|
||||
if st, ok := n.(sql.SchemaTarget); ok {
|
||||
sch = st.TargetSchema()
|
||||
}
|
||||
switch n := n.(type) {
|
||||
case *plan.ModifyColumn:
|
||||
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
|
||||
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
|
||||
validator = sv
|
||||
}
|
||||
}
|
||||
keyedColumns, err = analyzer.GetTableIndexColumns(ctx, n.Table)
|
||||
return false
|
||||
case *plan.RenameColumn:
|
||||
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
|
||||
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
|
||||
validator = sv
|
||||
}
|
||||
}
|
||||
return false
|
||||
case *plan.AddColumn:
|
||||
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
|
||||
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
|
||||
validator = sv
|
||||
}
|
||||
}
|
||||
keyedColumns, err = analyzer.GetTableIndexColumns(ctx, n.Table)
|
||||
return false
|
||||
case *plan.DropColumn:
|
||||
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
|
||||
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
|
||||
validator = sv
|
||||
}
|
||||
}
|
||||
return false
|
||||
case *plan.AlterIndex:
|
||||
if rt, ok := n.Table.(*plan.ResolvedTable); ok {
|
||||
if sv, ok := rt.UnwrappedDatabase().(sql.SchemaValidator); ok {
|
||||
validator = sv
|
||||
}
|
||||
}
|
||||
indexes, err = analyzer.GetTableIndexNames(ctx, a, n.Table)
|
||||
default:
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
// Skip this validation if we didn't find one or more of the above node types
|
||||
if len(sch) == 0 {
|
||||
return n, transform.SameTree, nil
|
||||
}
|
||||
|
||||
sch = sch.Copy() // Make a copy of the original schema to deal with any references to the original table.
|
||||
initialSch := sch
|
||||
|
||||
// Need a TransformUp here because multiple of these statement types can be nested under a Block node.
|
||||
// It doesn't look it, but this is actually an iterative loop over all the independent clauses in an ALTER statement
|
||||
n, same, err := transform.Node(ctx, n, func(ctx *sql.Context, n sql.Node) (sql.Node, transform.TreeIdentity, error) {
|
||||
switch nn := n.(type) {
|
||||
case *plan.ModifyColumn:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
sch, err = analyzer.ValidateModifyColumn(ctx, initialSch, sch, n.(*plan.ModifyColumn), keyedColumns)
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.RenameColumn:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
sch, err = analyzer.ValidateRenameColumn(ctx, initialSch, sch, n.(*plan.RenameColumn))
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.AddColumn:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
sch, err = analyzer.ValidateAddColumn(ctx, sch, n.(*plan.AddColumn))
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.DropColumn:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
sch, err = analyzer.ValidateDropColumn(ctx, initialSch, sch, n.(*plan.DropColumn))
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
delete(keyedColumns, nn.Column)
|
||||
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.AlterIndex:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
indexes, err = validateAlterIndex(ctx, initialSch, sch, n.(*plan.AlterIndex), indexes)
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
keyedColumns = analyzer.UpdateKeyedColumns(keyedColumns, nn)
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.AlterPK:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
sch, err = validatePrimaryKey(ctx, initialSch, sch, n.(*plan.AlterPK))
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.AlterDefaultSet:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
sch, err = analyzer.ValidateAlterDefault(ctx, initialSch, sch, n.(*plan.AlterDefaultSet))
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
return n, transform.NewTree, nil
|
||||
case *plan.AlterDefaultDrop:
|
||||
n, err := nn.WithTargetSchema(sch.Copy())
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
sch, err = analyzer.ValidateDropDefault(ctx, initialSch, sch, n.(*plan.AlterDefaultDrop))
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
return n, transform.NewTree, nil
|
||||
}
|
||||
return n, transform.SameTree, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
|
||||
if validator != nil {
|
||||
if err := validator.ValidateSchema(sch); err != nil {
|
||||
return nil, transform.SameTree, err
|
||||
}
|
||||
}
|
||||
|
||||
return n, same, nil
|
||||
}
|
||||
|
||||
// Returns the underlying table name for the node given
|
||||
func getTableName(node sql.Node) string {
|
||||
var tableName string
|
||||
transform.Inspect(node, func(node sql.Node) bool {
|
||||
switch node := node.(type) {
|
||||
case *plan.TableAlias:
|
||||
tableName = node.Name()
|
||||
return false
|
||||
case *plan.ResolvedTable:
|
||||
tableName = node.Name()
|
||||
return false
|
||||
case *plan.UnresolvedTable:
|
||||
tableName = node.Name()
|
||||
return false
|
||||
case *plan.IndexedTableAccess:
|
||||
tableName = node.Name()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return tableName
|
||||
}
|
||||
|
||||
// validatePrimaryKey validates a primary key add or drop operation.
|
||||
func validatePrimaryKey(ctx *sql.Context, initialSch, sch sql.Schema, ai *plan.AlterPK) (sql.Schema, error) {
|
||||
tableName := getTableName(ai.Table)
|
||||
switch ai.Action {
|
||||
case plan.PrimaryKeyAction_Create:
|
||||
if analyzer.HasPrimaryKeys(sch) {
|
||||
return nil, sql.ErrMultiplePrimaryKeysDefined.New()
|
||||
}
|
||||
|
||||
colMap := schToColMap(sch)
|
||||
idxDef := &sql.IndexDef{
|
||||
Name: "PRIMARY",
|
||||
Columns: ai.Columns,
|
||||
Constraint: sql.IndexConstraint_Primary,
|
||||
}
|
||||
|
||||
err := validateIndex(ctx, colMap, idxDef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, idxCol := range ai.Columns {
|
||||
schCol := colMap[strings.ToLower(idxCol.Name)]
|
||||
if schCol.Virtual {
|
||||
return nil, sql.ErrVirtualColumnPrimaryKey.New()
|
||||
}
|
||||
}
|
||||
|
||||
// Set the primary keys
|
||||
for _, col := range ai.Columns {
|
||||
sch[sch.IndexOf(col.Name, tableName)].PrimaryKey = true
|
||||
}
|
||||
|
||||
return sch, nil
|
||||
case plan.PrimaryKeyAction_Drop:
|
||||
if !analyzer.HasPrimaryKeys(sch) {
|
||||
return nil, sql.ErrCantDropFieldOrKey.New("PRIMARY")
|
||||
}
|
||||
|
||||
for _, col := range sch {
|
||||
if col.PrimaryKey {
|
||||
col.PrimaryKey = false
|
||||
}
|
||||
}
|
||||
|
||||
return sch, nil
|
||||
default:
|
||||
return sch, nil
|
||||
}
|
||||
}
|
||||
|
||||
// validateAlterIndex validates the specified column can have an index added, dropped, or renamed. Returns an updated
|
||||
// list of index name given the add, drop, or rename operations.
|
||||
func validateAlterIndex(ctx *sql.Context, initialSch, sch sql.Schema, ai *plan.AlterIndex, indexes []string) ([]string, error) {
|
||||
switch ai.Action {
|
||||
case plan.IndexAction_Create:
|
||||
err := analyzer.ValidateIdentifier(ai.IndexName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colMap := schToColMap(sch)
|
||||
|
||||
// TODO: plan.AlterIndex should just have a sql.IndexDef
|
||||
indexDef := &sql.IndexDef{
|
||||
Name: ai.IndexName,
|
||||
Columns: ai.Columns,
|
||||
Constraint: ai.Constraint,
|
||||
Storage: ai.Using,
|
||||
Comment: ai.Comment,
|
||||
}
|
||||
|
||||
err = validateIndex(ctx, colMap, indexDef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(indexes, ai.IndexName), nil
|
||||
case plan.IndexAction_Drop:
|
||||
savedIdx := -1
|
||||
for i, idx := range indexes {
|
||||
if strings.EqualFold(idx, ai.IndexName) {
|
||||
savedIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if savedIdx == -1 {
|
||||
return nil, sql.ErrCantDropFieldOrKey.New(ai.IndexName)
|
||||
}
|
||||
// Remove the index from the list
|
||||
return append(indexes[:savedIdx], indexes[savedIdx+1:]...), nil
|
||||
case plan.IndexAction_Rename:
|
||||
err := analyzer.ValidateIdentifier(ai.IndexName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
savedIdx := -1
|
||||
for i, idx := range indexes {
|
||||
if strings.EqualFold(idx, ai.PreviousIndexName) {
|
||||
savedIdx = i
|
||||
}
|
||||
}
|
||||
if savedIdx == -1 {
|
||||
return nil, sql.ErrCantDropFieldOrKey.New(ai.IndexName)
|
||||
}
|
||||
// Simulate the rename by deleting the old name and adding the new one.
|
||||
return append(append(indexes[:savedIdx], indexes[savedIdx+1:]...), ai.IndexName), nil
|
||||
}
|
||||
|
||||
return indexes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user