chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
@@ -0,0 +1,59 @@
// 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"
)
// applyTablesForAnalyzeAllTables finds plan.AnalyzeTable nodes that don't have any tables explicitly specified and fills in all
// tables for the current database. This enables the ANALYZE; statement to analyze all tables.
func applyTablesForAnalyzeAllTables(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
analyzeTable, ok := node.(*plan.AnalyzeTable)
if !ok {
return node, transform.SameTree, nil
}
// If a set of tables is already populated, we don't need to do anything. We only fill in all tables when
// the caller didn't explicitly specify any tables to be analyzed.
if len(analyzeTable.Tables) > 0 {
return node, transform.SameTree, nil
}
db, err := a.Catalog.Database(ctx, ctx.GetCurrentDatabase())
if err != nil {
return node, transform.SameTree, err
}
tableNames, err := db.GetTableNames(ctx)
if err != nil {
return node, transform.SameTree, err
}
var tables []sql.Table
for _, tableName := range tableNames {
table, ok, err := db.GetTableInsensitive(ctx, tableName)
if err != nil {
return node, transform.SameTree, err
} else if !ok {
return node, transform.SameTree, sql.ErrTableNotFound.New(tableName)
}
tables = append(tables, table)
}
return analyzeTable.WithTables(tables), transform.NewTree, nil
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2024 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/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/go-mysql-server/sql/types"
pgexprs "github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// AssignInsertCasts adds the appropriate assign casts for insertions.
func AssignInsertCasts(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
insertInto, ok := node.(*plan.InsertInto)
if !ok {
return node, transform.SameTree, nil
}
// We have some sources that are already postgres native, so skip them
if isDoltgresNativeSource(ctx, insertInto.Destination, insertInto.Source) {
return insertInto, transform.SameTree, nil
}
// First we'll make a map for each column, so that it's easier to match a name to a type. We also ensure that the
// types use Doltgres types, as casts rely on them. At this point, we shouldn't have any GMS types floating around
// anymore, so no need to include a lot of additional code to handle them.
destinationNameToType := make(map[string]*pgtypes.DoltgresType)
for _, col := range insertInto.Destination.Schema(ctx) {
colType, ok := col.Type.(*pgtypes.DoltgresType)
if !ok {
// Only non-Doltgres destination tables will have GMS types (such as system tables), so we don't error here
colType = pgtypes.FromGmsType(col.Type)
}
destinationNameToType[strings.ToLower(col.Name)] = colType
}
// Create the destination type slice that will match each inserted column
destinationTypes := make([]*pgtypes.DoltgresType, len(insertInto.ColumnNames))
for i, colName := range insertInto.ColumnNames {
destinationTypes[i], ok = destinationNameToType[strings.ToLower(colName)]
if !ok {
return nil, transform.NewTree, errors.Errorf("INSERT: cannot find destination column with name `%s`", colName)
}
}
// Replace expressions with casts as needed
if values, ok := insertInto.Source.(*plan.Values); ok {
// Values do not return the correct Schema since each row may contain different types, so we must handle it differently
newValues := make([][]sql.Expression, len(values.ExpressionTuples))
for rowIndex, rowExprs := range values.ExpressionTuples {
newValues[rowIndex] = make([]sql.Expression, len(rowExprs))
for columnIndex, colExpr := range rowExprs {
// Null ColumnDefaultValues or empty DefaultValues are not properly typed in TypeSanitizer, so we must handle them here
colExprType := colExpr.Type(ctx)
if colExprType == nil || colExprType == types.Null {
colExprType = pgtypes.Unknown
}
fromColType, ok := colExprType.(*pgtypes.DoltgresType)
if !ok {
return nil, transform.NewTree, errors.Errorf("INSERT: non-Doltgres type found in values source: %s", fromColType.String())
}
toColType := destinationTypes[columnIndex]
// We only assign the existing expression if the types perfectly match (same parameters), otherwise we'll cast
if fromColType.Equals(toColType) {
newValues[rowIndex][columnIndex] = colExpr
} else {
newValues[rowIndex][columnIndex] = pgexprs.NewAssignmentCast(colExpr, fromColType, toColType)
}
}
}
insertInto = insertInto.WithSource(plan.NewValues(newValues))
} else {
sourceSchema := insertInto.Source.Schema(ctx)
projections := make([]sql.Expression, len(sourceSchema))
for i, col := range sourceSchema {
fromColType, ok := col.Type.(*pgtypes.DoltgresType)
if !ok {
return nil, transform.NewTree, errors.Errorf("INSERT: non-Doltgres type found in source: %s", fromColType.String())
}
toColType := destinationTypes[i]
getField := expression.NewGetField(i, fromColType, col.Name, true)
// We only assign the GetField if the types perfectly match (same parameters), otherwise we'll cast
if fromColType.Equals(toColType) {
projections[i] = getField
} else {
projections[i] = pgexprs.NewAssignmentCast(getField, fromColType, toColType)
}
}
insertInto = insertInto.WithSource(plan.NewProject(ctx, projections, insertInto.Source))
}
// handle on conflict clause if present
if insertInto.OnDupExprs.HasUpdates() {
newDupExprs, err := assignUpdateFieldCasts(ctx, insertInto.OnDupExprs.AllExpressions())
if err != nil {
return nil, false, err
}
// TODO: this relies on a particular implementation detail InsertInto.WithExpressions
newInsertInto, err := insertInto.WithExpressions(ctx, append(newDupExprs, insertInto.Checks().ToExpressions()...)...)
if err != nil {
return nil, false, err
}
insertInto = newInsertInto.(*plan.InsertInto)
}
return insertInto, transform.NewTree, nil
}
func isDoltgresNativeSource(ctx *sql.Context, dest sql.Node, source sql.Node) bool {
// we still need this transformation if our destination has generated columns
for _, col := range dest.Schema(ctx) {
if col.Generated != nil {
return false
}
}
switch source.(type) {
case *node.CopyFrom:
return true
default:
return false
}
}
+241
View File
@@ -0,0 +1,241 @@
// Copyright 2024 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"
"sort"
"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"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/triggers"
pgexprs "github.com/dolthub/doltgresql/server/expression"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtransform "github.com/dolthub/doltgresql/server/transform"
)
// AssignTriggers assigns triggers wherever they're needed.
func AssignTriggers(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return pgtransform.NodeWithOpaque(ctx, node, func(ctx *sql.Context, node sql.Node) (sql.Node, transform.TreeIdentity, error) {
switch node := node.(type) {
case *plan.DeleteFrom, *plan.InsertInto, *plan.Truncate, *plan.Update:
sch, beforeTrigs, afterTrigs, err := getTriggerInformation(ctx, node)
if err != nil {
return nil, transform.NewTree, err
}
if len(beforeTrigs) == 0 && len(afterTrigs) == 0 {
return node, transform.SameTree, nil
}
newNode := node
if len(beforeTrigs) > 0 {
handling := getTriggerRowHandling(node)
newNode, err = nodeWithTriggers(ctx, newNode, &pgnodes.TriggerExecution{
Timing: triggers.TriggerTiming_Before,
Triggers: beforeTrigs,
Split: handling,
Return: handling,
Sch: sch,
Source: getTriggerSource(node),
Runner: pgexprs.StatementRunner{Runner: a.Runner},
})
if err != nil {
return nil, transform.NewTree, err
}
}
if len(afterTrigs) > 0 {
newNode = &pgnodes.TriggerExecution{
Timing: triggers.TriggerTiming_After,
Triggers: afterTrigs,
Split: getTriggerRowHandling(node),
Return: pgnodes.TriggerExecutionRowHandling_None,
Sch: sch,
Source: newNode,
Runner: pgexprs.StatementRunner{Runner: a.Runner},
}
}
return newNode, transform.NewTree, nil
default:
return node, transform.SameTree, nil
}
})
}
// getTriggerInformation loads information that is common for the different trigger types.
func getTriggerInformation(ctx *sql.Context, node sql.Node) (sch sql.Schema, beforeTrigs []triggers.Trigger, afterTrigs []triggers.Trigger, err error) {
var tbl sql.Table
switch node := node.(type) {
case *plan.DeleteFrom:
tbl, err = plan.GetDeletable(node.Child)
if err != nil {
return nil, nil, nil, err
}
case *plan.InsertInto:
tbl, err = plan.GetInsertable(node.Destination)
if err != nil {
return nil, nil, nil, err
}
case *plan.Truncate:
tbl, err = plan.GetTruncatable(node.Child)
if err != nil {
return nil, nil, nil, err
}
case *plan.Update:
// TODO: If there is a JoinNode in here, then don't bother calling GetUpdatable, because
// it doesn't currently return a type that can be used to query trigger information.
// We need to rework the plan.GetUpdatable() API to support returning multiple
// update targets and to return types that are compatible with the interfaces
// Doltgres needs in order to populate trigger information.
if hasJoinNode(node) {
return nil, nil, nil, nil
}
tbl, err = plan.GetUpdatable(node.Child)
if err != nil {
return nil, nil, nil, err
}
default:
return nil, nil, nil, nil
}
dbName := ctx.GetCurrentDatabase()
// TODO: some dolt tables don't implement this interface, so we use the current db for now
// An alternative would be to get the resolved table and use the db there.
schTbl, ok := tbl.(sql.DatabaseSchemaTable)
if ok {
dbName = schTbl.DatabaseSchema().Name()
}
trigCollection, err := core.GetTriggersCollectionFromContext(ctx, dbName)
if err != nil {
return nil, nil, nil, err
}
tblID, ok, _ := id.GetFromTable(ctx, tbl)
if !ok {
return nil, nil, nil, nil
}
allTrigs := trigCollection.GetTriggersForTable(ctx, tblID)
// Return early if there are no triggers for the table
if len(allTrigs) == 0 {
return tbl.Schema(ctx), nil, nil, nil
}
// Trigger order is determined by the name
sort.Slice(allTrigs, func(i, j int) bool {
return allTrigs[i].ID.TriggerName() < allTrigs[j].ID.TriggerName()
})
beforeTrigs = make([]triggers.Trigger, 0, len(allTrigs))
afterTrigs = make([]triggers.Trigger, 0, len(allTrigs))
for _, trig := range allTrigs {
matchesEventType := false
for _, event := range trig.Events {
switch node.(type) {
case *plan.DeleteFrom:
if event.Type == triggers.TriggerEventType_Delete {
matchesEventType = true
}
case *plan.InsertInto:
if event.Type == triggers.TriggerEventType_Insert {
matchesEventType = true
}
case *plan.Truncate:
if event.Type == triggers.TriggerEventType_Truncate {
matchesEventType = true
}
case *plan.Update:
if event.Type == triggers.TriggerEventType_Update {
matchesEventType = true
}
}
}
if !matchesEventType {
continue
}
switch trig.Timing {
case triggers.TriggerTiming_Before:
beforeTrigs = append(beforeTrigs, trig)
case triggers.TriggerTiming_After:
afterTrigs = append(afterTrigs, trig)
default:
return nil, nil, nil, fmt.Errorf("trigger timing has not yet been implemented")
}
}
return tbl.Schema(ctx), beforeTrigs, afterTrigs, nil
}
// hasJoinNode returns true if |node| or any child is a JoinNode.
func hasJoinNode(node sql.Node) bool {
updateJoinFound := false
transform.Inspect(node, func(n sql.Node) bool {
if _, ok := n.(*plan.JoinNode); ok {
updateJoinFound = true
}
return !updateJoinFound
})
return updateJoinFound
}
// getTriggerSource returns the trigger's source node.
func getTriggerSource(node sql.Node) sql.Node {
switch node := node.(type) {
case *plan.DeleteFrom:
return node.Child
case *plan.InsertInto:
return node.Source
case *plan.Truncate:
return node.Child
case *plan.Update:
return node.Child
default:
return node
}
}
// getTriggerRowHandling returns the trigger's row handling type (based on how GMS passes rows in the intermediate
// steps).
func getTriggerRowHandling(node sql.Node) pgnodes.TriggerExecutionRowHandling {
switch node.(type) {
case *plan.DeleteFrom:
return pgnodes.TriggerExecutionRowHandling_Old
case *plan.InsertInto:
return pgnodes.TriggerExecutionRowHandling_New
case *plan.Truncate:
return pgnodes.TriggerExecutionRowHandling_None
case *plan.Update:
return pgnodes.TriggerExecutionRowHandling_OldNew
default:
return pgnodes.TriggerExecutionRowHandling_None
}
}
// nodeWithTriggers calls the appropriate WithX function depending on the node type.
func nodeWithTriggers(ctx *sql.Context, node sql.Node, executionNode *pgnodes.TriggerExecution) (sql.Node, error) {
switch node := node.(type) {
case *plan.DeleteFrom:
return node.WithChildren(ctx, executionNode)
case *plan.InsertInto:
return node.WithSource(executionNode), nil
case *plan.Truncate:
return node.WithChildren(ctx, executionNode)
case *plan.Update:
return node.WithChildren(ctx, executionNode)
default:
return nil, fmt.Errorf("unknown node for triggers")
}
}
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2024 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"
"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/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
pgexprs "github.com/dolthub/doltgresql/server/expression"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// AssignUpdateCasts adds the appropriate assign casts for updates.
func AssignUpdateCasts(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
update, ok := node.(*plan.Update)
if !ok {
return node, transform.SameTree, nil
}
var newUpdate sql.Node
switch child := update.Child.(type) {
case *plan.UpdateSource:
newUpdateSource, err := assignUpdateCastsHandleSource(ctx, child)
if err != nil {
return nil, transform.NewTree, err
}
newUpdate, err = update.WithChildren(ctx, newUpdateSource)
if err != nil {
return nil, transform.NewTree, err
}
case *plan.ForeignKeyHandler:
updateSource, ok := child.OriginalNode.(*plan.UpdateSource)
if !ok {
return nil, transform.NewTree, errors.Errorf("UPDATE: assumption that Foreign Key child is always UpdateSource is incorrect: %T", child.OriginalNode)
}
newUpdateSource, err := assignUpdateCastsHandleSource(ctx, updateSource)
if err != nil {
return nil, transform.NewTree, err
}
newHandler, err := child.WithChildren(ctx, newUpdateSource)
if err != nil {
return nil, transform.NewTree, err
}
newUpdate, err = update.WithChildren(ctx, newHandler)
if err != nil {
return nil, transform.NewTree, err
}
case *plan.UpdateJoin:
updateSource, ok := child.Child.(*plan.UpdateSource)
if !ok {
return nil, transform.NewTree, fmt.Errorf("UPDATE: unknown source type: %T", child.Child)
}
newUpdateSource, err := assignUpdateCastsHandleSource(ctx, updateSource)
if err != nil {
return nil, transform.NewTree, err
}
newHandler, err := child.WithChildren(ctx, newUpdateSource)
if err != nil {
return nil, transform.NewTree, err
}
newUpdate, err = update.WithChildren(ctx, newHandler)
if err != nil {
return nil, transform.NewTree, err
}
default:
return nil, transform.NewTree, errors.Errorf("UPDATE: unknown source type: %T", child)
}
return newUpdate, transform.NewTree, nil
}
// assignUpdateCastsHandleSource handles the *plan.UpdateSource portion of AssignUpdateCasts.
func assignUpdateCastsHandleSource(ctx *sql.Context, updateSource *plan.UpdateSource) (*plan.UpdateSource, error) {
updateExprs := updateSource.UpdateExprs
newUpdateExprs, err := assignUpdateFieldCasts(ctx, updateExprs.AllExpressions())
if err != nil {
return nil, err
}
newUpdateSource, err := updateSource.WithExpressions(ctx, newUpdateExprs...)
if err != nil {
return nil, err
}
return newUpdateSource.(*plan.UpdateSource), nil
}
func assignUpdateFieldCasts(ctx *sql.Context, updateExprs []sql.Expression) ([]sql.Expression, error) {
newUpdateExprs := make([]sql.Expression, len(updateExprs))
for i, updateExpr := range updateExprs {
setField, ok := updateExpr.(*expression.SetField)
if !ok {
return nil, errors.Errorf("UPDATE: assumption that expression is always SetField is incorrect: %T", updateExpr)
}
fromType, ok := setField.RightChild.Type(ctx).(*pgtypes.DoltgresType)
if !ok {
return nil, errors.Errorf("UPDATE: non-Doltgres type found in source: %s", setField.RightChild.String())
}
toType, ok := setField.LeftChild.Type(ctx).(*pgtypes.DoltgresType)
if !ok {
// Only non-Doltgres destination tables will have GMS types (such as system tables), so we don't error here
toType = pgtypes.FromGmsType(setField.LeftChild.Type(ctx))
}
// We only assign the existing expression if the types perfectly match (same parameters), otherwise we'll cast
if fromType.Equals(toType) {
newUpdateExprs[i] = setField
} else {
newSetField, err := setField.WithChildren(ctx, setField.LeftChild, pgexprs.NewAssignmentCast(setField.RightChild, fromType, toType))
if err != nil {
return nil, err
}
newUpdateExprs[i] = newSetField
}
}
return newUpdateExprs, nil
}
@@ -0,0 +1,59 @@
// 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(ctx, n, func(ctx *sql.Context, 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" {
alterDropPk := plan.NewAlterDropPk(rt.Database(), rt)
newNode, err := alterDropPk.WithTargetSchema(rt.Schema(ctx))
if err != nil {
return n, transform.SameTree, err
}
return newNode, transform.NewTree, nil
}
}
}
return n, transform.SameTree, nil
})
}
+56
View File
@@ -0,0 +1,56 @@
// 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/planbuilder"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/doltgresql/server/node"
)
// ValidateCreateFunction validates that a function can be created as specified. It validates functions defined
// with SQL language.
func ValidateCreateFunction(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.(*node.CreateFunction)
if !ok {
return n, transform.SameTree, nil
}
if len(ct.SqlDef) == 0 {
return n, transform.SameTree, nil
}
checkFunctionBodiesVar, err := ctx.GetSessionVariable(ctx, "check_function_bodies")
if err != nil {
return n, transform.SameTree, err
}
if checkFunctionBodiesVar.(int8) == 0 {
return n, transform.SameTree, nil
}
builder := planbuilder.New(ctx, a.Catalog, nil)
for _, parsed := range ct.SqlDefParsedStmts {
_, _, err = builder.BindOnly(parsed, ct.SqlDef, nil)
if err != nil {
return nil, transform.SameTree, err
}
}
return n, transform.SameTree, nil
}
+237
View File
@@ -0,0 +1,237 @@
// Copyright 2024 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/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/planbuilder"
"github.com/dolthub/go-mysql-server/sql/transform"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/postgres/parser/parser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
"github.com/dolthub/doltgresql/server/ast"
"github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/node"
pgtransform "github.com/dolthub/doltgresql/server/transform"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// AddDomainConstraints adds domain type's default value and check constraints
// to the destination table schema and InsertNode/Update node's checks.
func AddDomainConstraints(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
switch n := node.(type) {
case *plan.InsertInto:
return loadDomainConstraints(ctx, a, n, n.Schema(ctx))
case *plan.Update:
return loadDomainConstraints(ctx, a, n, n.Schema(ctx))
default:
return node, transform.SameTree, nil
}
}
// loadDomainConstraints retrieves and assigns domain type's default value, nullable and check constraints
// to the destination table schema and InsertNode/Update node's checks.
func loadDomainConstraints(ctx *sql.Context, a *analyzer.Analyzer, c sql.CheckConstraintNode, schema sql.Schema) (sql.Node, transform.TreeIdentity, error) {
// get current checks to append the domain checks to.
checks := c.Checks()
var same = transform.SameTree
for _, col := range schema {
if dt, ok := col.Type.(*pgtypes.DoltgresType); ok && dt.TypType == pgtypes.TypeType_Domain {
// assign column nullable
col.Nullable = !dt.NotNull
// get domain default value and assign to the column default value
defVal, err := getDomainDefault(ctx, a, dt.Default, col.Source, col.Type, col.Nullable)
if err != nil {
return nil, transform.SameTree, err
}
col.Default = defVal
// get domain checks
colChecks, err := getDomainCheckConstraintsForTable(ctx, a, col.Name, col.Source, dt.Checks)
if err != nil {
return nil, transform.SameTree, err
}
checks = append(checks, colChecks...)
same = transform.NewTree
}
}
if same == transform.SameTree {
return c, same, nil
}
return c.WithChecks(checks), same, nil
}
// getDomainDefault takes the default value definition, parses, builds and returns sql.ColumnDefaultValue.
func getDomainDefault(ctx *sql.Context, a *analyzer.Analyzer, defExpr, tblName string, typ sql.Type, nullable bool) (*sql.ColumnDefaultValue, error) {
if defExpr == "" {
return nil, nil
}
parsed, err := a.Parser.ParseSimple(fmt.Sprintf("select %s from %s", defExpr, tblName))
if err != nil {
return nil, err
}
selectStmt, ok := parsed.(*vitess.Select)
if !ok || len(selectStmt.SelectExprs) != 1 {
return nil, sql.ErrInvalidColumnDefaultValue.New(defExpr)
}
expr := selectStmt.SelectExprs[0]
ae, ok := expr.(*vitess.AliasedExpr)
if !ok {
return nil, sql.ErrInvalidColumnDefaultValue.New(defExpr)
}
builder := planbuilder.New(ctx, a.Catalog, nil)
return builder.BuildColumnDefaultValueWithTable(ae.Expr, selectStmt.From[0], typ, nullable), nil
}
// getDomainCheckConstraintsForTable takes the check constraint definitions, parses, builds and returns sql.CheckConstraints.
func getDomainCheckConstraintsForTable(ctx *sql.Context, a *analyzer.Analyzer, colName string, tblName string, checkDefs []*sql.CheckDefinition) (sql.CheckConstraints, error) {
checks := make(sql.CheckConstraints, len(checkDefs))
for i, check := range checkDefs {
q := fmt.Sprintf("select %s from %s", check.CheckExpression, tblName)
checkExpr, err := parseAndReplaceDomainCheckConstraint(ctx, a, check.CheckExpression, q, &tree.ColumnItem{
ColumnName: tree.Name(colName),
TableName: &tree.UnresolvedObjectName{NumParts: 1, Parts: [3]string{tblName}},
})
if err != nil {
return nil, err
}
checks[i] = &sql.CheckConstraint{
Name: check.Name,
Expr: checkExpr,
Enforced: true,
}
}
return checks, nil
}
// AddDomainConstraintsToCasts adds domain type's constraints to cast expressions.
func AddDomainConstraintsToCasts(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return pgtransform.NodeExprsWithOpaque(ctx, node, func(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
var same = transform.SameTree
switch e := expr.(type) {
case *expression.ExplicitCast:
if rt, ok := e.Type(ctx).(*pgtypes.DoltgresType); ok && rt.TypType == pgtypes.TypeType_Domain {
// the domain type should be resolved by this point
colChecks, err := getDomainCheckConstraintsForCast(ctx, a, rt.Checks, e.Child())
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
expr = e.WithDomainConstraints(!rt.NotNull, colChecks)
}
return expr, same, nil
default:
// TODO: add ASSIGNMENT, IMPLICIT cast and other expressions that use domain types
return e, transform.SameTree, nil
}
})
}
// getDomainCheckConstraintsForCast takes the check constraint definitions, parses, builds and returns sql.CheckConstraints.
func getDomainCheckConstraintsForCast(ctx *sql.Context, a *analyzer.Analyzer, checkDefs []*sql.CheckDefinition, value sql.Expression) (sql.CheckConstraints, error) {
checks := make(sql.CheckConstraints, len(checkDefs))
for i, check := range checkDefs {
q := fmt.Sprintf("select %s", check.CheckExpression)
checkExpr, err := parseAndReplaceDomainCheckConstraint(ctx, a, check.CheckExpression, q, tree.DomainColumn{})
if err != nil {
return nil, err
}
// replace DomainColumn with given sql.Expression
checkExpr, _, _ = transform.Expr(ctx, checkExpr, func(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
switch e := expr.(type) {
case *node.DomainColumn:
expr = value
return expr, transform.NewTree, nil
default:
return e, transform.SameTree, nil
}
})
checks[i] = &sql.CheckConstraint{
Name: check.Name,
Expr: checkExpr,
Enforced: true,
}
}
return checks, nil
}
// parseAndReplaceDomainCheckConstraint parses check constraint and replaces the `VALUE` column
// reference with given Expr including resolved column given table and column names or DomainColumn.
// It returns built check expression.
func parseAndReplaceDomainCheckConstraint(ctx *sql.Context, a *analyzer.Analyzer, checkExpr, query string, replacesValue tree.Expr) (sql.Expression, error) {
stmt, err := parser.ParseOne(query)
if err != nil {
return nil, err
}
selectStmt, ok := stmt.AST.(*tree.Select)
if !ok {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
selectClause, ok := selectStmt.Select.(*tree.SelectClause)
if !ok {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
exprs := selectClause.Exprs
if len(exprs) != 1 {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
updatedCheckExpr, err := tree.SimpleVisit(exprs[0].Expr, func(visitingExpr tree.Expr) (recurse bool, newExpr tree.Expr, err error) {
switch v := visitingExpr.(type) {
case *tree.UnresolvedName:
if strings.ToLower(v.String()) != "value" {
return false, nil, errors.Errorf(`column "%s" does not exist`, v.String())
}
return false, replacesValue, nil
}
return true, visitingExpr, nil
})
if err != nil {
return nil, err
}
exprs[0].Expr = updatedCheckExpr
parsed, err := ast.Convert(stmt)
if err != nil {
return nil, err
}
convertedSelectStmt, ok := parsed.(*vitess.Select)
if !ok || len(convertedSelectStmt.SelectExprs) != 1 {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
expr := convertedSelectStmt.SelectExprs[0]
ae, ok := expr.(*vitess.AliasedExpr)
if !ok {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
builder := planbuilder.New(ctx, a.Catalog, nil)
var tblExpr vitess.TableExpr
if len(convertedSelectStmt.From) == 1 {
tblExpr = convertedSelectStmt.From[0]
}
return builder.BuildScalarWithTable(ae.Expr, tblExpr), nil
}
+85
View File
@@ -0,0 +1,85 @@
// 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/expression"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/casts"
"github.com/dolthub/doltgresql/server/functions/framework"
"github.com/dolthub/doltgresql/server/types"
)
// validateForeignKeyDefinition validates that the given foreign key definition is valid for creation
func validateForeignKeyDefinition(ctx *sql.Context, fkDef sql.ForeignKeyConstraint, cols map[string]*sql.Column, parentCols map[string]*sql.Column) error {
var castsColl *casts.Collection
if len(fkDef.Columns) > 0 {
var err error
// TODO: which database is this supposed to use?
castsColl, err = core.GetCastsCollectionFromContext(ctx, "")
if err != nil {
return err
}
}
for i := range fkDef.Columns {
col := cols[strings.ToLower(fkDef.Columns[i])]
parentCol := parentCols[strings.ToLower(fkDef.ParentColumns[i])]
if !foreignKeyComparableTypes(ctx, castsColl, col.Type, parentCol.Type) {
return errors.Errorf("Key columns %q and %q are of incompatible types: %s and %s", col.Name, parentCol.Name, col.Type.String(), parentCol.Type.String())
}
}
return nil
}
// foreignKeyComparableTypes returns whether the two given types are able to be used as parent/child columns in a
// foreign key.
func foreignKeyComparableTypes(ctx *sql.Context, castColl *casts.Collection, from sql.Type, to sql.Type) bool {
dtFrom, ok := from.(*types.DoltgresType)
if !ok {
return false // should never be possible
}
dtTo, ok := to.(*types.DoltgresType)
if !ok {
return false // should never be possible
}
if dtFrom.Equals(dtTo) {
return true
}
fromLiteral := expression.NewLiteral(dtFrom.Zero(), from)
toLiteral := expression.NewLiteral(dtTo.Zero(), to)
// a foreign key between two different types is valid if there is an equality operator on the two types
// TODO: there are some subtleties in postgres not captured by this logic, e.g. a foreign key from double -> int
// is valid, but the reverse is not. This works fine, but is more permissive than postgres is.
eq := framework.GetBinaryFunction(framework.Operator_BinaryEqual).Compile(ctx, "=", fromLiteral, toLiteral)
if eq == nil || eq.StashedError() != nil {
return false
}
// Additionally, we need to be able to convert freely between the two types in both directions, since we do this
// during the process of enforcing the constraints
forwardConversion, fErr := castColl.GetAssignmentCast(ctx, dtFrom, dtTo)
reverseConversion, rErr := castColl.GetAssignmentCast(ctx, dtTo, dtFrom)
return fErr == nil && rErr == nil && forwardConversion.ID.IsValid() && reverseConversion.ID.IsValid()
}
+122
View File
@@ -0,0 +1,122 @@
// 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"
"github.com/dolthub/doltgresql/server/functions"
)
// 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(ctx *sql.Context, _ *analyzer.Analyzer, n sql.Node, _ *plan.Scope, _ analyzer.RuleSelector, _ *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return transform.Node(ctx, n, func(ctx *sql.Context, 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(ctx, n.Name(), fk)
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(ctx, copiedFk.Table, &copiedFk)
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"). If an existing foreign key is found with the default, generated
// name, the generated name will be suffixed with a number to ensure uniqueness.
func generateFkName(ctx *sql.Context, tableName string, newFk *sql.ForeignKeyConstraint) (string, error) {
columnNames := strings.Join(newFk.Columns, "_")
generatedBaseName := fmt.Sprintf("%s_%s_fkey", tableName, columnNames)
for counter := 0; counter < 100; counter += 1 {
generatedFkName := generatedBaseName
if counter > 0 {
generatedFkName = fmt.Sprintf("%s%d", generatedBaseName, counter)
}
duplicate := false
err := functions.IterateCurrentDatabase(ctx, functions.Callbacks{
ForeignKey: func(ctx *sql.Context, schema functions.ItemSchema, table functions.ItemTable, foreignKey functions.ItemForeignKey) (cont bool, err error) {
if foreignKey.Item.Name == generatedFkName {
duplicate = true
return false, nil
}
return true, nil
},
})
if err != nil {
return "", err
}
if !duplicate {
return generatedFkName, nil
}
}
return "", fmt.Errorf("unable to create unique foreign key %s: "+
"a foreign key constraint already exists with this name", generatedBaseName)
}
+200
View File
@@ -0,0 +1,200 @@
// Copyright 2024 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/analyzer"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/planbuilder"
pgexpression "github.com/dolthub/doltgresql/server/expression"
)
// IDs are basically arbitrary, we just need to ensure that they do not conflict with existing IDs
// 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_ApplyTablesForAnalyzeAllTables // applyTablesForAnalyzeAllTables
ruleId_AssignInsertCasts // assignInsertCasts
ruleId_AssignTriggers // assignTriggers
ruleId_AssignUpdateCasts // assignUpdateCasts
ruleId_ConvertDropPrimaryKeyConstraint // convertDropPrimaryKeyConstraint
ruleId_GenerateForeignKeyName // generateForeignKeyName
ruleId_ReplaceIndexedTables // replaceIndexedTables
ruleId_ReplaceNode // replaceNode
ruleId_TransformRecordFilter // transformRecordFilter
ruleId_ReplaceSerial // replaceSerial
ruleId_InsertContextRootFinalizer // insertContextRootFinalizer
ruleId_ResolveType // resolveType
ruleId_ReplaceArithmeticExpressions // replaceArithmeticExpressions
ruleId_OptimizeFunctions // optimizeFunctions
ruleId_ValidateColumnDefaults // validateColumnDefaults
ruleId_ValidateCreateTable // validateCreateTable
ruleId_ValidateCreateSchema // validateCreateSchema
ruleId_ResolveAlterColumn // resolveAlterColumn
ruleId_ValidateCreateFunction // validateCreateFunction
ruleId_ResolveValuesTypes // resolveValuesTypes
ruleId_ResolveProcedureDefaults // resolveProcedureDefaults
ruleId_SetRunner // setRunner
)
// Init adds additional rules to the analyzer to handle Doltgres-specific functionality.
func Init() {
// OnceBeforeDefault runs before AlwaysBeforeDefault in GMS
analyzer.OnceBeforeDefault = append([]analyzer.Rule{
{Id: ruleId_ResolveType, Apply: ResolveType}, // ResolveType rule must run before simplifyFilters rule in GMS
{Id: ruleId_ApplyTablesForAnalyzeAllTables, Apply: applyTablesForAnalyzeAllTables},
{Id: ruleId_ConvertDropPrimaryKeyConstraint, Apply: convertDropPrimaryKeyConstraint}},
analyzer.OnceBeforeDefault...)
analyzer.AlwaysBeforeDefault = append(analyzer.AlwaysBeforeDefault,
// ResolveType rule must run in this batch in addition to OnceBeforeDefault batch
// because of custom batch set optimization in GMS skipping OnceBeforeDefault batch for some nodes.
analyzer.Rule{Id: ruleId_ResolveType, Apply: ResolveType},
analyzer.Rule{Id: ruleId_SetRunner, Apply: SetRunner},
analyzer.Rule{Id: ruleId_TypeSanitizer, Apply: TypeSanitizer},
analyzer.Rule{Id: ruleId_ResolveValuesTypes, Apply: ResolveValuesTypes},
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},
analyzer.Rule{Id: ruleId_AssignUpdateCasts, Apply: AssignUpdateCasts},
analyzer.Rule{Id: ruleId_AssignTriggers, Apply: AssignTriggers},
analyzer.Rule{Id: ruleId_ValidateCreateFunction, Apply: ValidateCreateFunction},
analyzer.Rule{Id: ruleId_ValidateCreateSchema, Apply: ValidateCreateSchema},
analyzer.Rule{Id: ruleId_ResolveProcedureDefaults, Apply: ResolveProcedureDefaults},
)
// 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: ruleId_ResolveAlterColumn, Apply: resolveAlterColumn})
analyzer.OnceBeforeDefault = removeAnalyzerRules(
analyzer.OnceBeforeDefault,
analyzer.ValidateColumnDefaultsId,
analyzer.ValidateCreateTableId,
analyzer.ResolveAlterColumnId,
)
// Remove all other validation rules that do not apply to Postgres
analyzer.DefaultValidationRules = removeAnalyzerRules(analyzer.DefaultValidationRules, analyzer.ValidateOperandsId)
analyzer.DefaultRules = append(analyzer.DefaultRules,
analyzer.Rule{Id: ruleId_TransformRecordFilter, Apply: TransformRecordFilter},
)
analyzer.OnceAfterDefault = append(analyzer.OnceAfterDefault,
analyzer.Rule{Id: ruleId_ReplaceSerial, Apply: ReplaceSerial},
analyzer.Rule{Id: ruleId_ReplaceArithmeticExpressions, Apply: ReplaceArithmeticExpressions},
)
// The auto-commit rule writes the contents of the context, so we need to insert our finalizer before that.
// We also should optimize functions last, since other rules may change the underlying expressions, potentially changing their return types.
analyzer.OnceAfterAll = insertAnalyzerRules(analyzer.OnceAfterAll, analyzer.QuoteDefaultColumnValueNamesId, false,
analyzer.Rule{Id: ruleId_OptimizeFunctions, Apply: OptimizeFunctions},
// AddDomainConstraintsToCasts needs to run after 'assignExecIndexes' rule in GMS.
analyzer.Rule{Id: ruleId_AddDomainConstraintsToCasts, Apply: AddDomainConstraintsToCasts},
analyzer.Rule{Id: ruleId_ReplaceNode, Apply: ReplaceNode},
analyzer.Rule{Id: ruleId_InsertContextRootFinalizer, Apply: InsertContextRootFinalizer},
)
initEngine()
}
// TODO: introduce a real pluggable architecture for this instead of swapping function pointers
func initEngine() {
// This technically takes place at execution time rather than as part of analysis, but we don't have a better
// place to put it. Our foreign key validation logic is different from MySQL's, and since it's not an analyzer rule
// we can't swap out a rule like the rest of the logic in this package, we have to do a function swap.
plan.ValidateForeignKeyDefinition = validateForeignKeyDefinition
planbuilder.IsAggregateFunc = IsAggregateFunc
expression.DefaultExpressionFactory = pgexpression.PostgresExpressionFactory{}
expression.SplitConjunction = splitConjunction
}
// IsAggregateFunc checks if the given function name is an aggregate function. This is the entire set supported by
// MySQL plus some postgres specific ones.
func IsAggregateFunc(name string) bool {
if planbuilder.IsMySQLAggregateFuncName(name) {
return true
}
switch name {
case "array_agg", "bool_and", "bool_or":
return true
}
return false
}
// insertAnalyzerRules inserts the given rule(s) before or after the given analyzer.RuleId, returning an updated slice.
func insertAnalyzerRules(rules []analyzer.Rule, id analyzer.RuleId, before bool, additionalRules ...analyzer.Rule) []analyzer.Rule {
inserted := false
newRules := make([]analyzer.Rule, len(rules)+len(additionalRules))
for i, rule := range rules {
if rule.Id == id {
inserted = true
if before {
copy(newRules, rules[:i])
copy(newRules[i:], additionalRules)
copy(newRules[i+len(additionalRules):], rules[i:])
} else {
copy(newRules, rules[:i+1])
copy(newRules[i+1:], additionalRules)
copy(newRules[i+1+len(additionalRules):], rules[i+1:])
}
break
}
}
if !inserted {
panic("no rules were inserted")
}
return newRules
}
// removeAnalyzerRules removes the given analyzer.RuleId(s), returning an updated slice.
func removeAnalyzerRules(rules []analyzer.Rule, remove ...analyzer.RuleId) []analyzer.Rule {
ids := make(map[analyzer.RuleId]struct{})
for _, removal := range remove {
ids[removal] = struct{}{}
}
removedIds := 0
var newRules []analyzer.Rule
for _, rule := range rules {
if _, ok := ids[rule.Id]; !ok {
newRules = append(newRules, rule)
} else {
removedIds++
}
}
if removedIds < len(remove) {
panic("one or more rules were not removed, this is a bug")
}
return newRules
}
@@ -0,0 +1,70 @@
// Copyright 2024 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"
pgnodes "github.com/dolthub/doltgresql/server/node"
)
// InsertContextRootFinalizer inserts a ContextRootFinalizer node right before the transaction commits, yet after all
// other nodes have finished. This ensures that the ContextRootFinalizer does not overwrite any changes from its
// children.
func InsertContextRootFinalizer(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
if _, ok := node.(*pgnodes.ContextRootFinalizer); ok {
return node, transform.SameTree, nil
}
// Analysis may occur separately on child nodes, so we have to ensure that only one finalizer exists in the tree
newNode, _, err := transform.NodeWithOpaque(ctx, node, transformRemoveContextRootFinalizer)
if err != nil {
return nil, transform.NewTree, err
}
return pgnodes.NewContextRootFinalizer(newNode), transform.NewTree, nil
}
// transformRemoveContextRootFinalizer is the function used by the transform from within InsertContextRootFinalizer.
func transformRemoveContextRootFinalizer(ctx *sql.Context, node sql.Node) (sql.Node, transform.TreeIdentity, error) {
if finalizer, ok := node.(*pgnodes.ContextRootFinalizer); ok {
return finalizer.Child(), transform.NewTree, nil
} else if disjointedNode, ok := node.(plan.DisjointedChildrenNode); ok {
var err error
same := transform.SameTree
disjointedChildGroups := disjointedNode.DisjointedChildren()
newDisjointedChildGroups := make([][]sql.Node, len(disjointedChildGroups))
for groupIdx, disjointedChildGroup := range disjointedChildGroups {
newDisjointedChildGroups[groupIdx] = make([]sql.Node, len(disjointedChildGroup))
for childIdx, disjointedChild := range disjointedChildGroup {
var childIdentity transform.TreeIdentity
newDisjointedChildGroups[groupIdx][childIdx], childIdentity, err = transform.NodeWithOpaque(ctx, disjointedChild, transformRemoveContextRootFinalizer)
if err != nil {
return nil, transform.NewTree, err
}
same = same && childIdentity
}
}
if same == transform.NewTree {
if newChild, err := disjointedNode.WithDisjointedChildren(newDisjointedChildGroups); err != nil {
return nil, transform.NewTree, err
} else {
return newChild, transform.NewTree, nil
}
}
}
return node, transform.SameTree, nil
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2024 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"
"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/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/planbuilder"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtransform "github.com/dolthub/doltgresql/server/transform"
)
// OptimizeFunctions replaces all functions that fit specific criteria with their optimized variants. Also handles
// SRFs (set-returning functions) by setting the `IncludesNestedIters` flag on the Project node if any SRF is found
// inside projection expressions.
func OptimizeFunctions(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
// This is supposed to be one of the last rules to run. Subqueries break that assumption, so we skip this rule in such cases.
if scope != nil && scope.CurrentNodeIsFromSubqueryExpression {
return node, transform.SameTree, nil
}
_, isInsertNode := node.(*plan.InsertInto)
return pgtransform.NodeWithOpaque(ctx, node, func(ctx *sql.Context, n sql.Node) (sql.Node, transform.TreeIdentity, error) {
projectNode, ok := n.(*plan.Project)
if !ok {
return n, transform.SameTree, nil
}
hasMultipleExpressionTuples := false
hasSRF := false
// Check if there is set returning function in the source node (e.g. SELECT * FROM unnest())
n, sameNode, err := transform.NodeExprsWithNode(ctx, projectNode.Child, func(ctx *sql.Context, in sql.Node, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
if compiledFunction, ok := expr.(*framework.CompiledFunction); ok {
// TODO: need better way to detect sequence usage
switch compiledFunction.FunctionName() {
case "nextval", "setval", "currval":
err := authCheckSequenceFromExpr(ctx, a.Catalog.AuthHandler, compiledFunction.Arguments[0])
if err != nil {
return nil, transform.SameTree, err
}
}
hasSRF = hasSRF || compiledFunction.IsSRF()
if quickFunction := compiledFunction.GetQuickFunction(); quickFunction != nil {
return quickFunction, transform.NewTree, nil
}
// fill in default exprs if applicable
if err := compiledFunction.ResolveDefaultValues(ctx, func(defExpr string) (sql.Expression, error) {
return getDefaultExpr(ctx, a.Catalog, defExpr)
}); err != nil {
return nil, transform.SameTree, err
}
}
if v, ok := in.(*plan.Values); ok {
hasMultipleExpressionTuples = len(v.ExpressionTuples) > 1
}
return expr, transform.SameTree, nil
})
if err != nil {
return nil, transform.SameTree, err
}
if !sameNode {
projectNode.Child = n
}
// insert node cannot have more than 1 row value if it has set returning function
if isInsertNode && hasMultipleExpressionTuples && hasSRF {
return nil, false, errors.Errorf("set-returning functions are not allowed in VALUES")
}
// Check if there is set returning function in the projection expressions (e.g. SELECT unnest() [FROM table/srf])
hasSRFInProjection := false
exprs, sameExprs, err := transform.Exprs(ctx, projectNode.Projections, func(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
if compiledFunction, ok := expr.(*framework.CompiledFunction); ok {
hasSRFInProjection = hasSRFInProjection || compiledFunction.IsSRF()
if quickFunction := compiledFunction.GetQuickFunction(); quickFunction != nil {
return quickFunction, transform.NewTree, nil
}
// TODO: need better way to detect sequence usage
switch compiledFunction.FunctionName() {
case "nextval", "setval", "currval":
err = authCheckSequenceFromExpr(ctx, a.Catalog.AuthHandler, compiledFunction.Arguments[0])
if err != nil {
return nil, transform.SameTree, err
}
}
// fill in default exprs if applicablea
if err = compiledFunction.ResolveDefaultValues(ctx, func(defExpr string) (sql.Expression, error) {
return getDefaultExpr(ctx, a.Catalog, defExpr)
}); err != nil {
return nil, transform.SameTree, err
}
}
return expr, transform.SameTree, nil
})
if err != nil {
return nil, transform.SameTree, err
}
if !sameExprs {
projectNode.Projections = exprs
}
// nested iter is used for set returning functions in the projections only
if hasSRFInProjection {
// Under some conditions, there will be no quick-function replacement, but changing the Project node to include
// nested iterators is still a change we need to tell the transform functions about.
sameExprs = transform.NewTree
projectNode = projectNode.WithIncludesNestedIters(true)
}
return projectNode, sameNode && sameExprs, err
})
}
// getDefaultExpr takes the default value definition, parses, builds and returns sql.ColumnDefaultValue.
func getDefaultExpr(ctx *sql.Context, c sql.Catalog, defExpr string) (sql.Expression, error) {
builder := planbuilder.New(ctx, c, nil)
proj, _, _, _, err := builder.Parse(fmt.Sprintf("select %s", defExpr), nil, false)
if err != nil {
return nil, err
}
parsedExpr := proj.(*plan.Project).Projections[0]
if a, ok := parsedExpr.(*expression.Alias); ok {
parsedExpr = a.Child
}
return parsedExpr, nil
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2024 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"
gms_expression "github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/functions/framework"
)
// ReplaceArithmeticExpressions replaces arithmetic expressions in the tree with the equivalent binary operators.
// This is a problem in certain nodes that do arithmetic, such as TopN, but all nodes are covered.
func ReplaceArithmeticExpressions(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return transform.NodeExprsWithOpaque(ctx, node, func(ctx *sql.Context, e sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
switch e := e.(type) {
case *gms_expression.Arithmetic:
switch e.Operator() {
case "+":
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryPlus).WithChildren(ctx, e.Children()...)
if err != nil {
return nil, transform.NewTree, err
}
return expr.(*expression.BinaryOperator), transform.NewTree, nil
case "-":
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryMinus).WithChildren(ctx, e.Children()...)
if err != nil {
return nil, transform.NewTree, err
}
return expr.(*expression.BinaryOperator), transform.NewTree, nil
case "*":
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryMultiply).WithChildren(ctx, e.Children()...)
if err != nil {
return nil, transform.NewTree, err
}
return expr.(*expression.BinaryOperator), transform.NewTree, nil
case "/":
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryDivide).WithChildren(ctx, e.Children()...)
if err != nil {
return nil, transform.NewTree, err
}
return expr.(*expression.BinaryOperator), transform.NewTree, nil
}
}
return e, transform.SameTree, nil
})
}
+36
View File
@@ -0,0 +1,36 @@
// 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"
pgnodes "github.com/dolthub/doltgresql/server/node"
)
// ReplaceNode is used to replace generic top-level nodes with Doltgres versions that wrap them, without performing any
// additional analysis. This is used to handle relatively straightforward tasks, like delete cascading, etc.
func ReplaceNode(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
// TODO: need to add the majority of other DDL operations here
switch node := node.(type) {
case *plan.DropTable:
return pgnodes.NewDropTable(node), transform.NewTree, nil
default:
return node, transform.SameTree, nil
}
}
+131
View File
@@ -0,0 +1,131 @@
// 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 analyzer
import (
"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"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/extensions"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/functions"
"github.com/dolthub/doltgresql/server/functions/framework"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// ResolveProcedureDefaults resolves default expressions of routines that are in string format by parsing it into sql.Expression.
// This function retrieves the procedure overloads and sets CompiledFunction in the Call node.
func ResolveProcedureDefaults(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
switch n := node.(type) {
case *pgnodes.Call:
procCollection, err := core.GetProceduresCollectionFromContext(ctx, "")
if err != nil {
return nil, transform.SameTree, err
}
typesCollection, err := core.GetTypesCollectionFromContext(ctx, "")
if err != nil {
return nil, transform.SameTree, err
}
schemaName, err := core.GetSchemaName(ctx, nil, n.SchemaName)
if err != nil {
return nil, transform.SameTree, err
}
procName := id.NewProcedure(schemaName, n.ProcedureName)
overloads, err := procCollection.GetProcedureOverloads(ctx, procName)
if err != nil {
return nil, transform.SameTree, err
}
if len(overloads) == 0 {
if strings.HasPrefix(n.ProcedureName, "dolt_") {
return nil, transform.SameTree, functions.ErrDoltProcedureSelectOnly
}
return nil, transform.SameTree, sql.ErrStoredProcedureDoesNotExist.New(n.ProcedureName)
}
same := transform.SameTree
overloadTree := framework.NewOverloads()
for _, overload := range overloads {
paramTypes := make([]*pgtypes.DoltgresType, len(overload.ParameterTypes))
for i, paramType := range overload.ParameterTypes {
paramTypes[i], err = typesCollection.GetType(ctx, paramType)
if err != nil || paramTypes[i] == nil {
return nil, transform.SameTree, err
}
}
// TODO: we should probably have procedure equivalents instead of converting these to functions
// probably fine for now since we don't implement/support the differing functionality between the two just yet
if len(overload.ExtensionName) > 0 {
if err = overloadTree.Add(framework.CFunction{
ID: id.Function(overload.ID),
ReturnType: pgtypes.Void,
ParameterTypes: paramTypes,
Variadic: false,
IsNonDeterministic: true,
Strict: false,
ExtensionName: extensions.LibraryIdentifier(overload.ExtensionName),
ExtensionSymbol: overload.ExtensionSymbol,
}); err != nil {
return nil, transform.SameTree, err
}
} else if len(overload.SQLDefinition) > 0 {
if err = overloadTree.Add(framework.SQLFunction{
ID: id.Function(overload.ID),
ReturnType: pgtypes.Void,
ParameterNames: overload.ParameterNames,
ParameterTypes: paramTypes,
ParameterDefaults: overload.ParameterDefaults,
Variadic: false,
IsNonDeterministic: true,
Strict: false,
SqlStatement: overload.SQLDefinition,
SetOf: false,
}); err != nil {
return nil, transform.SameTree, err
}
} else {
if err = overloadTree.Add(framework.InterpretedFunction{
ID: id.Function(overload.ID),
ReturnType: pgtypes.Void,
ParameterNames: overload.ParameterNames,
ParameterTypes: paramTypes,
Variadic: false,
IsNonDeterministic: true,
Strict: false,
SRF: false,
Statements: overload.Operations,
}); err != nil {
return nil, transform.SameTree, err
}
}
}
compiledFunction := framework.NewCompiledFunction(ctx, n.ProcedureName, n.Exprs, overloadTree, false)
// fill in default exprs if applicable
if err := compiledFunction.ResolveDefaultValues(ctx, func(defExpr string) (sql.Expression, error) {
return getDefaultExpr(ctx, a.Catalog, defExpr)
}); err != nil {
return nil, transform.SameTree, err
}
n.CompiledFunc = compiledFunction
return node, same, nil
default:
return node, transform.SameTree, nil
}
}
+304
View File
@@ -0,0 +1,304 @@
// Copyright 2024 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/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"
pgnodes "github.com/dolthub/doltgresql/server/node"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
pgexprs "github.com/dolthub/doltgresql/server/expression"
pgtransform "github.com/dolthub/doltgresql/server/transform"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// ResolveType replaces types.ResolvableType to appropriate pgtypes.DoltgresType.
func ResolveType(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
n, sameExpr, err := ResolveTypeForExprs(ctx, a, node, scope, selector, qFlags)
if err != nil {
return nil, transform.NewTree, err
}
n, sameNode, err := ResolveTypeForNodes(ctx, a, n, scope, selector, qFlags)
if err != nil {
return nil, transform.NewTree, err
}
return n, sameExpr && sameNode, nil
}
// ResolveTypeForNodes replaces types.ResolvableType to appropriate pgtypes.DoltgresType.
func ResolveTypeForNodes(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
var db sql.Database
if dbnode, ok := node.(sql.Databaser); ok {
db = dbnode.Database()
}
return transform.Node(ctx, node, func(ctx *sql.Context, node sql.Node) (sql.Node, transform.TreeIdentity, error) {
var same = transform.SameTree
switch n := node.(type) {
case *plan.AddColumn:
col := n.Column()
if rt, ok := col.Type.(*pgtypes.DoltgresType); ok && !rt.IsResolvedType() {
dt, err := resolveType(ctx, db, rt)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
col.Type = dt
}
return node, same, nil
case *pgnodes.CreateCast:
if !n.Source.IsResolvedType() {
source, err := resolveType(ctx, db, n.Source)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Source = source
}
if !n.Target.IsResolvedType() {
target, err := resolveType(ctx, db, n.Target)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Target = target
}
for i, param := range n.FuncParams {
if !param.Type.IsResolvedType() {
target, err := resolveType(ctx, db, param.Type)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.FuncParams[i].Type = target
}
}
return node, same, nil
case *pgnodes.CreateFunction:
if !n.ReturnType.IsResolvedType() {
retType, err := resolveType(ctx, db, n.ReturnType)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.ReturnType = retType
}
for i, param := range n.Parameters {
if !param.Type.IsResolvedType() {
paramType, err := resolveType(ctx, db, param.Type)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Parameters[i].Type = paramType
}
}
return node, same, nil
case *pgnodes.CreateProcedure:
for i := range n.Parameters {
var err error
n.Parameters[i].Type, err = resolveType(ctx, db, n.Parameters[i].Type)
if err != nil {
return nil, transform.NewTree, err
}
}
return node, transform.NewTree, nil
case *plan.CreateTable:
for _, col := range n.TargetSchema() {
if rt, ok := col.Type.(*pgtypes.DoltgresType); ok && !rt.IsResolvedType() {
dt, err := resolveType(ctx, db, rt)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
col.Type = dt
}
resolvedDefault, err := resolveDefaultColumnType(ctx, db, col.Default)
if err != nil {
return nil, transform.NewTree, err
}
resolvedGenerated, err := resolveDefaultColumnType(ctx, db, col.Generated)
if err != nil {
return nil, transform.NewTree, err
}
resolvedOnUpdate, err := resolveDefaultColumnType(ctx, db, col.OnUpdate)
if err != nil {
return nil, transform.NewTree, err
}
if resolvedDefault || resolvedGenerated || resolvedOnUpdate {
same = transform.NewTree
}
}
return node, same, nil
case *pgnodes.DropCast:
if !n.Source.IsResolvedType() {
source, err := resolveType(ctx, db, n.Source)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Source = source
}
if !n.Target.IsResolvedType() {
target, err := resolveType(ctx, db, n.Target)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
n.Target = target
}
return node, same, nil
case *pgnodes.DropFunction:
for _, r := range n.RoutinesWithArgs {
for j, arg := range r.Args {
if !arg.Type.IsResolvedType() {
dt, err := resolveType(ctx, db, arg.Type)
if err != nil {
// the type can be non-existing type
continue
}
same = transform.NewTree
r.Args[j].Type = dt
}
}
}
return node, same, nil
case *pgnodes.DropProcedure:
for _, r := range n.RoutinesWithArgs {
for j, arg := range r.Args {
if !arg.Type.IsResolvedType() {
dt, err := resolveType(ctx, db, arg.Type)
if err != nil {
// the type can be non-existing type
continue
}
same = transform.NewTree
r.Args[j].Type = dt
}
}
}
return node, same, nil
case *plan.ModifyColumn:
col := n.NewColumn()
if rt, ok := col.Type.(*pgtypes.DoltgresType); ok && !rt.IsResolvedType() {
dt, err := resolveType(ctx, db, rt)
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
col.Type = dt
}
return node, same, nil
default:
// TODO: add nodes that use unresolved types like domain
return node, transform.SameTree, nil
}
})
}
// ResolveTypeForExprs replaces types.ResolvableType to appropriate pgtypes.DoltgresType.
func ResolveTypeForExprs(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
var db sql.Database
if dbnode, ok := node.(sql.Databaser); ok {
db = dbnode.Database()
}
return pgtransform.NodeExprsWithOpaque(ctx, node, func(ctx *sql.Context, e sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
switch expr := e.(type) {
case *pgexprs.ColumnAccess:
exprType, _ := expr.Type(ctx).(*pgtypes.DoltgresType)
if exprType == nil {
return nil, transform.NewTree, errors.New("column access is missing its child expression")
} else if exprType.IsResolvedType() {
// The type has already been resolved
return expr, transform.SameTree, nil
}
newType, err := resolveType(ctx, db, exprType)
if err != nil {
return nil, transform.NewTree, err
}
return expr.WithType(newType), transform.NewTree, nil
case *pgnodes.FunctionColumn:
if expr.Typ.IsResolvedType() {
// The type has already been resolved
return expr, transform.SameTree, nil
}
newType, err := resolveType(ctx, db, expr.Typ)
if err != nil {
return nil, transform.NewTree, err
}
expr.Typ = newType
return expr, transform.NewTree, nil
default:
// TODO: add expressions that use unresolved types like domain
return expr, transform.SameTree, nil
}
})
}
// resolveType resolves any type that is unresolved yet. (e.g.: domain types, built-in types that schema specified, etc.)
func resolveType(ctx *sql.Context, db sql.Database, typ *pgtypes.DoltgresType) (*pgtypes.DoltgresType, error) {
if typ.IsResolvedType() {
return typ, nil
}
var dbname string
if db != nil {
dbname = db.Name()
}
typs, err := core.GetTypesCollectionFromContext(ctx, dbname)
if err != nil {
return nil, err
}
// schema name can be empty
schema, _ := core.GetSchemaName(ctx, db, typ.ID.SchemaName())
resolvedType, _ := typs.GetType(ctx, id.NewType(schema, typ.ID.TypeName()))
if resolvedType == nil {
// If a blank schema is provided, then we'll also try the pg_catalog, since a type is most likely to be there
if typ.ID.SchemaName() == "" {
resolvedType, err = typs.GetType(ctx, id.NewType("pg_catalog", typ.ID.TypeName()))
if err != nil {
return nil, err
}
if resolvedType != nil && (typ.ID.TypeName() == "unknown" || resolvedType.ID != pgtypes.Unknown.ID) {
return resolvedType, nil
}
}
return nil, pgtypes.ErrTypeDoesNotExist.New(typ.Name())
}
return resolvedType, nil
}
// resolveDefaultColumnType resolves the OutType of a *sql.ColumnDefaultValue if it's not nil (and not already resolved).
func resolveDefaultColumnType(ctx *sql.Context, db sql.Database, defaultVal *sql.ColumnDefaultValue) (bool, error) {
if defaultVal == nil {
return false, nil
}
if rt, ok := defaultVal.OutType.(*pgtypes.DoltgresType); ok && !rt.IsResolvedType() {
dt, err := resolveType(ctx, db, rt)
if err != nil {
return false, err
}
defaultVal.OutType = dt
return true, nil
}
return false, nil
}
+240
View File
@@ -0,0 +1,240 @@
// 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 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/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
pgexprs "github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtransform "github.com/dolthub/doltgresql/server/transform"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// ResolveValuesTypes determines the common type for each column in a VALUES clause
// by examining all rows, following PostgreSQL's type resolution rules.
// This ensures VALUES(1),(2.01),(3) correctly infers numeric type, not integer.
func ResolveValuesTypes(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
// Walk the tree and wrap mixed-type VALUES columns with ImplicitCast.
// We record which VDTs changed so we can fix up GetField types afterward.
transformedVDTs := make(map[sql.TableId]sql.Schema)
node, same, err := transform.NodeWithOpaque(ctx, node, func(ctx *sql.Context, n sql.Node) (sql.Node, transform.TreeIdentity, error) {
newNode, same, err := transformValuesNode(ctx, n)
if err != nil {
return nil, same, err
}
if !same {
if vdt, ok := newNode.(*plan.ValueDerivedTable); ok {
transformedVDTs[vdt.Id()] = vdt.Schema(ctx)
}
}
return newNode, same, err
})
if err != nil {
return nil, transform.SameTree, err
}
// Now, fix GetField types that reference a transformed VDT. For example,
// after wrapping VALUES(1),(2.5) with ImplicitCast to numeric, any
// GetField reading column "n" from that VDT still says int4 and needs
// to be updated to numeric.
if len(transformedVDTs) > 0 {
node, _, err = pgtransform.NodeExprsWithOpaque(ctx, node, func(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
gf, ok := expr.(*expression.GetField)
if !ok {
return expr, transform.SameTree, nil
}
newSch, ok := transformedVDTs[gf.TableId()]
if !ok {
return expr, transform.SameTree, nil
}
// We match by column name because GetField indices are global
// across all tables in a JOIN (e.g., a.n=0, b.id=1, b.label=2).
// We can't convert a global index to a per-table position without
// knowing the table's starting offset, which we don't have here.
schemaIdx := -1
for i, col := range newSch {
if col.Name == gf.Name() {
schemaIdx = i
break
}
}
if schemaIdx < 0 {
return expr, transform.SameTree, nil
}
newType := newSch[schemaIdx].Type
if gf.Type(ctx) == newType {
return expr, transform.SameTree, nil
}
return expression.NewGetFieldWithTable(
gf.Index(), int(gf.TableId()), newType,
gf.Database(), gf.Table(), gf.Name(), gf.IsNullable(ctx),
), transform.NewTree, nil
})
if err != nil {
return nil, transform.SameTree, err
}
// The pass above only fixed GetFields that read directly from a VDT
// (matched by tableId). But changing a VDT column's type can have a
// ripple effect: if that column feeds into an aggregate like MIN or
// MAX, the aggregate's return type changes too. Parent nodes that
// read the aggregate result still have the old type. For example:
//
// SELECT MIN(n) FROM (VALUES(1),(2.5)) v(n)
//
// Project [GetField("min(v.n)", tableId=GroupBy, type=int4)]
// └── GroupBy [MIN(GetField("n", tableId=VDT, type=numeric))]
// └── VDT [n: int4 → numeric]
//
// The pass above fixed "n" inside MIN because its tableId=VDT.
// MIN now returns numeric, so GroupBy produces numeric. But the
// Project's GetField still says int4 because its tableId=GroupBy,
// which wasn't in transformedVDTs. At runtime this causes a panic
// because the actual value is *apd.Decimal but the type says int32.
//
// This pass catches those: for each GetField, check if its type
// disagrees with what the child node actually produces.
node, _, err = pgtransform.NodeExprsWithNodeWithOpaque(ctx, node, func(ctx *sql.Context, n sql.Node, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
gf, ok := expr.(*expression.GetField)
if !ok {
return expr, transform.SameTree, nil
}
// Skip VDT GetFields — the first pass already handled these
if _, isVDT := transformedVDTs[gf.TableId()]; isVDT {
return expr, transform.SameTree, nil
}
// Collect the schema that this node's children produce
var childSchema sql.Schema
for _, child := range n.Children() {
childSchema = append(childSchema, child.Schema(ctx)...)
}
// TODO: GMS is case-insensitive for identifiers, so aggregate
// GetField names and child schema names may differ in casing.
// We use strings.ToLower to handle this, but Postgres requires
// case-sensitivity for quoted identifiers, which this breaks.
gfName := strings.ToLower(gf.Name())
for _, col := range childSchema {
if strings.ToLower(col.Name) == gfName && gf.Type(ctx) != col.Type {
return expression.NewGetFieldWithTable(
gf.Index(), int(gf.TableId()), col.Type,
gf.Database(), gf.Table(), gf.Name(), gf.IsNullable(ctx),
), transform.NewTree, nil
}
}
return expr, transform.SameTree, nil
})
if err != nil {
return nil, transform.SameTree, err
}
}
return node, same, nil
}
// transformValuesNode transforms a plan.Values or plan.ValueDerivedTable node to use common types
func transformValuesNode(ctx *sql.Context, n sql.Node) (sql.Node, transform.TreeIdentity, error) {
var values *plan.Values
var expressionerNode sql.Expressioner
switch v := n.(type) {
case *plan.ValueDerivedTable:
values = v.Values
expressionerNode = v
case *plan.Values:
values = v
expressionerNode = v
default:
return n, transform.SameTree, nil
}
// Skip if no rows or single row (nothing to unify)
if len(values.ExpressionTuples) <= 1 {
return n, transform.SameTree, nil
}
numCols := len(values.ExpressionTuples[0])
for i := 1; i < len(values.ExpressionTuples); i++ {
if len(values.ExpressionTuples[i]) != numCols {
return nil, transform.NewTree, errors.Errorf("VALUES: row %d has %d columns, expected %d", i+1, len(values.ExpressionTuples[i]), numCols)
}
}
if numCols == 0 {
return n, transform.SameTree, nil
}
// Collect types for each column across all rows
columnTypes := make([][]*pgtypes.DoltgresType, numCols)
for colIdx := 0; colIdx < numCols; colIdx++ {
columnTypes[colIdx] = make([]*pgtypes.DoltgresType, len(values.ExpressionTuples))
for rowIdx, row := range values.ExpressionTuples {
exprType := row[colIdx].Type(ctx)
if exprType == nil {
columnTypes[colIdx][rowIdx] = pgtypes.Unknown
} else if pgType, ok := exprType.(*pgtypes.DoltgresType); ok {
columnTypes[colIdx][rowIdx] = pgType
} else {
return nil, transform.NewTree, errors.Errorf("VALUES: non-Doltgres type found in row %d, column %d: %s", rowIdx, colIdx, exprType.String())
}
}
}
// Find common type for each column
var newTuples [][]sql.Expression
for colIdx := 0; colIdx < numCols; colIdx++ {
commonType, requiresCasts, err := framework.FindCommonType(ctx, columnTypes[colIdx])
if err != nil {
return nil, transform.NewTree, err
}
// If we require any casts, then we'll add casting to all expressions in the list
if requiresCasts {
if len(newTuples) == 0 {
// Deep copy to avoid mutating the original expression tuples.
newTuples = make([][]sql.Expression, len(values.ExpressionTuples))
for i, row := range values.ExpressionTuples {
newTuples[i] = make([]sql.Expression, len(row))
copy(newTuples[i], row)
}
}
for rowIdx := 0; rowIdx < len(newTuples); rowIdx++ {
newTuples[rowIdx][colIdx] = pgexprs.NewImplicitCast(
newTuples[rowIdx][colIdx], columnTypes[colIdx][rowIdx], commonType)
}
}
}
// If we didn't require any casts, then we can simply return our old node
if len(newTuples) == 0 {
return n, transform.SameTree, nil
}
// Flatten the new tuples into a single expression slice for WithExpressions
flatExprs := make([]sql.Expression, 0, len(newTuples)*len(newTuples[0]))
for _, row := range newTuples {
flatExprs = append(flatExprs, row...)
}
newNode, err := expressionerNode.WithExpressions(ctx, flatExprs...)
if err != nil {
return nil, transform.NewTree, err
}
return newNode, transform.NewTree, nil
}
+209
View File
@@ -0,0 +1,209 @@
// Copyright 2024 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/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/analyzer"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/sequences"
"github.com/dolthub/doltgresql/server/ast"
"github.com/dolthub/doltgresql/server/auth"
pgexprs "github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/functions"
"github.com/dolthub/doltgresql/server/functions/framework"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// maxSequenceAutoNames is the maximum number of otherwise-identical sequence names that can be generated before
// resulting in an error. Under normal operation, sequence names should be automatically cleaned up when their table
// gets dropped, so except in extremely unusual circumstances this limit should never be reached. If it is, it's
// probably an indicator of a bug in sequence cleanup (or an extremely large schema).
const maxSequenceAutoNames = 10_000
// ReplaceSerial replaces a CreateTable node containing a SERIAL type with a node that can create sequences alongside
// the table.
func ReplaceSerial(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
createTable, ok := node.(*plan.CreateTable)
if !ok {
return node, transform.SameTree, nil
}
var ctSequences []*pgnodes.CreateSequence
for _, col := range createTable.PkSchema().Schema {
doltgresType, isDoltgresType := col.Type.(*pgtypes.DoltgresType)
if !isDoltgresType || !doltgresType.IsSerial {
continue
}
// For always-generated columns we insert a placeholder sequence to be replaced by the actual sequence name. We
// detect that here and treat these generated columns differently than other generated columns on serial types.
isGeneratedFromSequence := false
if col.Generated != nil {
seenNextVal := false
transform.InspectExpr(ctx, col.Generated, func(ctx *sql.Context, expr sql.Expression) bool {
switch e := expr.(type) {
case *framework.CompiledFunction:
if strings.ToLower(e.Name) == "nextval" {
seenNextVal = true
}
case *expression.Literal:
placeholderName := fmt.Sprintf("'%s'", ast.DoltCreateTablePlaceholderSequenceName)
if e.String() == placeholderName {
isGeneratedFromSequence = true
}
}
return false
})
if !seenNextVal && !isGeneratedFromSequence {
continue
}
}
schemaName, err := core.GetSchemaName(ctx, createTable.Db, "")
if err != nil {
return nil, false, err
}
sequenceName, err := generateSequenceName(ctx, createTable, col, schemaName)
if err != nil {
return nil, transform.NewTree, err
}
// TODO: need better way to detect sequence usage
err = authCheckSequence(ctx, a.Catalog.AuthHandler, schemaName, sequenceName)
if err != nil {
return nil, transform.SameTree, err
}
seqName := doltdb.TableName{Name: sequenceName, Schema: schemaName}.String()
nextVal, isDoltgresType, err := framework.GetFunction(ctx, "nextval", pgexprs.NewTextLiteral(seqName))
if err != nil {
return nil, transform.NewTree, err
}
if !isDoltgresType {
return nil, transform.NewTree, errors.Errorf(`function "nextval" could not be found for SERIAL default`)
}
nextValExpr := &sql.ColumnDefaultValue{
Expr: nextVal,
OutType: pgtypes.Int64,
Literal: false,
ReturnNil: false,
Parenthesized: false,
}
if isGeneratedFromSequence {
col.Generated = nextValExpr
} else {
col.Default = nextValExpr
}
var maxValue int64
switch doltgresType.Name() {
case "smallserial":
col.Type = pgtypes.Int16
maxValue = 32767
case "serial":
col.Type = pgtypes.Int32
maxValue = 2147483647
case "bigserial":
col.Type = pgtypes.Int64
maxValue = 9223372036854775807
}
ctSequences = append(ctSequences, pgnodes.NewCreateSequence(false, "", false, &sequences.Sequence{
Id: id.NewSequence("", sequenceName),
DataTypeID: col.Type.(*pgtypes.DoltgresType).ID,
Persistence: sequences.Persistence_Permanent,
Start: 1,
Current: 1,
Increment: 1,
Minimum: 1,
Maximum: maxValue,
Cache: 1,
Cycle: false,
IsAtEnd: false,
OwnerTable: id.NewTable("", createTable.Name()),
OwnerColumn: col.Name,
}))
}
return pgnodes.NewCreateTable(createTable, ctSequences), transform.NewTree, nil
}
// generateSequenceName generates a unique sequence name for a SERIAL column in the table given
func generateSequenceName(ctx *sql.Context, createTable *plan.CreateTable, col *sql.Column, schemaName string) (string, error) {
baseSequenceName := fmt.Sprintf("%s_%s_seq", createTable.Name(), col.Name)
sequenceName := baseSequenceName
relationType, err := core.GetRelationType(ctx, schemaName, baseSequenceName)
if err != nil {
return "", err
}
if relationType != core.RelationType_DoesNotExist {
seqIndex := 1
for ; seqIndex <= maxSequenceAutoNames; seqIndex++ {
sequenceName = fmt.Sprintf("%s%d", baseSequenceName, seqIndex)
relationType, err = core.GetRelationType(ctx, schemaName, sequenceName)
if err != nil {
return "", err
}
if relationType == core.RelationType_DoesNotExist {
break
}
}
if seqIndex > maxSequenceAutoNames {
return "", errors.Errorf("SERIAL sequence name reached max iterations")
}
}
return sequenceName, nil
}
// authCheckSequenceFromExpr checks authorization of sequence being used.
// It parses schema and sequence names out of given expression.
// There can be only one argument expression of string type.
func authCheckSequenceFromExpr(ctx *sql.Context, ah sql.AuthorizationHandler, arg sql.Expression) error {
schemaName, seqName, err := functions.ParseRelationName(ctx, strings.Trim(arg.String(), "'"))
if err != nil {
return err
}
return authCheckSequence(ctx, ah, schemaName, seqName)
}
// authCheckSequence checks authorization of sequence being used. We cannot check it during parsing because we cannot
// detect sequence currently, so we try to catch any sequence being used and check authorization here.
func authCheckSequence(ctx *sql.Context, ah sql.AuthorizationHandler, schemaName, seqName string) error {
if err := ah.HandleAuth(ctx, ah.NewQueryState(ctx), sqlparser.AuthInformation{
AuthType: auth.AuthType_USAGE,
TargetType: auth.AuthTargetType_SequenceIdentifiers,
TargetNames: []string{schemaName, seqName},
}); err != nil {
return err
}
return nil
}
+29
View File
@@ -0,0 +1,29 @@
// 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 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"
"github.com/dolthub/doltgresql/core"
)
// SetRunner sets the StatementRunner in the context.
func SetRunner(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return node, transform.SameTree, core.SetRunnerOnContext(ctx, a.Runner)
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2024 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/expression"
pgexprs "github.com/dolthub/doltgresql/server/expression"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// splitConjunction breaks AND expressions into their left and right parts, recursively.
func splitConjunction(ctx *sql.Context, expr sql.Expression) []sql.Expression {
if expr == nil {
return nil
}
switch expr := expr.(type) {
case *expression.And:
return append(
splitConjunction(ctx, expr.LeftChild),
splitConjunction(ctx, expr.RightChild)...,
)
case *pgexprs.GMSCast:
// We should check to see if we need to preserve the cast on each child individually
split := splitConjunction(ctx, expr.Child())
for i := range split {
if _, ok := split[i].Type(ctx).(*pgtypes.DoltgresType); !ok {
split[i] = pgexprs.NewGMSCast(split[i])
}
}
return split
default:
return []sql.Expression{expr}
}
}
// LogicTreeWalker is a walker that removes GMSCast and other Doltgres specific expression nodes from
// logic expression trees. This allows the analyzer logic to correctly reason about expressions in filters
// to apply indexes.
type LogicTreeWalker struct{}
var _ sql.ExpressionTreeFilter = &LogicTreeWalker{}
// Next implements the sql.ExpressionTreeFilter interface.
func (l *LogicTreeWalker) Next(e sql.Expression) sql.Expression {
switch expr := e.(type) {
case *pgexprs.GMSCast:
return l.Next(expr.Child())
default:
return e
}
}
+334
View File
@@ -0,0 +1,334 @@
// 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 analyzer
import (
"github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/functions/framework"
"github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/analyzer"
gmsexpr "github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
)
// TransformRecordFilter finds Filter nodes over Indexable tables with composite indexes over Record columns and
// decomposes the Record comparison into an equivalent filter expression over each column joined by ANDs and ORs.
func TransformRecordFilter(
ctx *sql.Context,
_ *analyzer.Analyzer,
node sql.Node,
_ *plan.Scope,
_ analyzer.RuleSelector,
_ *sql.QueryFlags,
) (sql.Node, transform.TreeIdentity, error) {
return transform.Node(ctx, node, func(ctx *sql.Context, n sql.Node) (sql.Node, transform.TreeIdentity, error) {
filter, ok := n.(*plan.Filter)
if !ok {
return n, transform.SameTree, nil
}
var tblNode sql.TableNode
switch child := filter.Child.(type) {
case *plan.ResolvedTable:
tblNode = child
case *plan.TableAlias:
tblNode, _ = child.Child.(sql.TableNode)
default:
return n, transform.SameTree, nil
}
// TODO: should only convert expressions when there's applicable index
if _, ok = tblNode.UnderlyingTable().(sql.IndexAddressableTable); !ok {
return n, transform.SameTree, nil
}
newExpr, same, err := decomposeRecordFilter(ctx, filter.Expression)
if err != nil {
return nil, transform.SameTree, err
}
if same {
return n, transform.SameTree, nil
}
return plan.NewFilter(ctx, newExpr, filter.Child), transform.NewTree, nil
})
}
// decomposeRecordFilter is the helper function to TransformRecordFilter that decomposes the Record comparison.
func decomposeRecordFilter(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
return transform.Expr(ctx, expr,
func(ctx *sql.Context, e sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
binExpr, ok := e.(*expression.BinaryOperator)
if !ok {
return e, transform.SameTree, nil
}
// TODO: possible for Literal to be on left
recExpr, ok := binExpr.Left().(*expression.RecordExpr)
if !ok {
return e, transform.SameTree, nil
}
recExprs := recExpr.Expressions()
// TODO: possible for RecordExpr to be on right
litExpr, ok := binExpr.Right().(*gmsexpr.Literal)
if !ok {
return e, transform.SameTree, nil
}
recVals, ok := litExpr.Val.([]types.RecordValue)
if !ok {
return e, transform.SameTree, nil
}
var newExpr sql.Expression
var err error
switch binExpr.Operator() {
case framework.Operator_BinaryEqual:
newExpr, err = decomposeRecordFilterEquals(ctx, recExprs, recVals)
case framework.Operator_BinaryLessThan:
newExpr, err = decomposeRecordFilterLessThan(ctx, recExprs, recVals)
case framework.Operator_BinaryGreaterThan:
newExpr, err = decomposeRecordFilterGreaterThan(ctx, recExprs, recVals)
case framework.Operator_BinaryLessOrEqual:
newExpr, err = decomposeRecordFilterLessThanEquals(ctx, recExprs, recVals)
case framework.Operator_BinaryGreaterOrEqual:
newExpr, err = decomposeRecordFilterGreaterThanEquals(ctx, recExprs, recVals)
default:
return e, transform.SameTree, nil
}
if err != nil {
return nil, transform.SameTree, err
}
return newExpr, transform.NewTree, nil
})
}
// decomposeRecordFilterEquals splits up (i, j, ...) = (x, y, ...) expression into (i = x) AND (j = y) AND ...
func decomposeRecordFilterEquals(
ctx *sql.Context,
recExprs []sql.Expression,
recVals []types.RecordValue,
) (sql.Expression, error) {
n := len(recExprs)
exprs := make([]sql.Expression, n)
for i := 0; i < n; i++ {
newLit := gmsexpr.NewLiteral(recVals[i].Value, recVals[i].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(ctx, recExprs[i], newLit)
if err != nil {
return nil, err
}
exprs[i] = expr
}
return gmsexpr.JoinAnd(exprs...), nil
}
// decomposeRecordFilterLessThan splits up (i, j, ..., k) < (x, y, ..., z) into ((i = x) OR (j = y) ... OR (k < z)) AND ...
func decomposeRecordFilterLessThan(
ctx *sql.Context,
recExprs []sql.Expression,
recVals []types.RecordValue,
) (sql.Expression, error) {
n := len(recExprs)
orExprs := make([]sql.Expression, n)
for i := 0; i < n; i++ {
andExprs := make([]sql.Expression, n-i)
for j := 0; j < n-i-1; j++ {
newLit := gmsexpr.NewLiteral(recVals[j].Value, recVals[j].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(
ctx,
recExprs[j],
newLit,
)
if err != nil {
return nil, err
}
andExprs[j] = expr
}
newLit := gmsexpr.NewLiteral(recVals[n-i-1].Value, recVals[n-i-1].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryLessThan).WithChildren(
ctx,
recExprs[i],
newLit,
)
if err != nil {
return nil, err
}
andExprs[n-i-1] = expr
orExprs[i] = gmsexpr.JoinAnd(andExprs...)
}
return gmsexpr.JoinOr(orExprs...), nil
}
// decomposeRecordFilterLessThan splits up (i, j, ..., k) > (x, y, ..., z) into ((i = x) OR (j = y) ... OR (k > z)) AND ...
func decomposeRecordFilterGreaterThan(
ctx *sql.Context,
recExprs []sql.Expression,
recVals []types.RecordValue,
) (sql.Expression, error) {
n := len(recExprs)
orExprs := make([]sql.Expression, n)
for i := 0; i < n; i++ {
andExprs := make([]sql.Expression, n-i)
for j := 0; j < n-i-1; j++ {
newLit := gmsexpr.NewLiteral(recVals[j].Value, recVals[j].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(
ctx,
recExprs[j],
newLit,
)
if err != nil {
return nil, err
}
andExprs[j] = expr
}
newLit := gmsexpr.NewLiteral(recVals[n-i-1].Value, recVals[n-i-1].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryGreaterThan).WithChildren(
ctx,
recExprs[n-i-1],
newLit,
)
if err != nil {
return nil, err
}
andExprs[n-i-1] = expr
orExprs[i] = gmsexpr.JoinAnd(andExprs...)
}
return gmsexpr.JoinOr(orExprs...), nil
}
// decomposeRecordFilterLessThanEquals splits up (i, j, ..., k) <= (x, y, ..., z) into ((i = x) OR (j = y) ... OR (k <= z)) AND ...
func decomposeRecordFilterLessThanEquals(
ctx *sql.Context,
recExprs []sql.Expression,
recVals []types.RecordValue,
) (sql.Expression, error) {
n := len(recExprs)
orExprs := make([]sql.Expression, n)
andExprs := make([]sql.Expression, n)
for i := 0; i < n-1; i++ {
newLit := gmsexpr.NewLiteral(recVals[i].Value, recVals[i].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(
ctx,
recExprs[i],
newLit,
)
if err != nil {
return nil, err
}
andExprs[i] = expr
}
newLit := gmsexpr.NewLiteral(recVals[n-1].Value, recVals[n-1].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryLessOrEqual).WithChildren(
ctx,
recExprs[n-1],
newLit,
)
if err != nil {
return nil, err
}
andExprs[n-1] = expr
orExprs[0] = gmsexpr.JoinAnd(andExprs...)
for i := 1; i < n; i++ {
andExprs = make([]sql.Expression, n-i)
for j := 0; j < n-i-1; j++ {
newLit = gmsexpr.NewLiteral(recVals[j].Value, recVals[j].Type)
expr, err = expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(
ctx,
recExprs[j],
newLit,
)
if err != nil {
return nil, err
}
andExprs[j] = expr
}
newLit = gmsexpr.NewLiteral(recVals[n-i-1].Value, recVals[n-i-1].Type)
expr, err = expression.NewBinaryOperator(framework.Operator_BinaryLessThan).WithChildren(
ctx,
recExprs[n-i-1],
newLit,
)
if err != nil {
return nil, err
}
andExprs[n-i-1] = expr
orExprs[i] = gmsexpr.JoinAnd(andExprs...)
}
return gmsexpr.JoinOr(orExprs...), nil
}
// decomposeRecordFilterGreaterThanEquals splits up (i, j, ..., k) >= (x, y, ..., z) into ((i = x) OR (j = y) ... OR (k >= z)) AND ...
func decomposeRecordFilterGreaterThanEquals(
ctx *sql.Context,
recExprs []sql.Expression,
recVals []types.RecordValue,
) (sql.Expression, error) {
n := len(recExprs)
orExprs := make([]sql.Expression, n)
andExprs := make([]sql.Expression, n)
for i := 0; i < n-1; i++ {
newLit := gmsexpr.NewLiteral(recVals[i].Value, recVals[i].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(
ctx,
recExprs[i],
newLit,
)
if err != nil {
return nil, err
}
andExprs[i] = expr
}
newLit := gmsexpr.NewLiteral(recVals[n-1].Value, recVals[n-1].Type)
expr, err := expression.NewBinaryOperator(framework.Operator_BinaryGreaterOrEqual).WithChildren(
ctx,
recExprs[n-1],
newLit,
)
if err != nil {
return nil, err
}
andExprs[n-1] = expr
orExprs[0] = gmsexpr.JoinAnd(andExprs...)
for i := 1; i < n; i++ {
andExprs = make([]sql.Expression, n-i)
for j := 0; j < n-i-1; j++ {
newLit = gmsexpr.NewLiteral(recVals[j].Value, recVals[j].Type)
expr, err = expression.NewBinaryOperator(framework.Operator_BinaryEqual).WithChildren(
ctx,
recExprs[j],
newLit,
)
if err != nil {
return nil, err
}
andExprs[j] = expr
}
newLit = gmsexpr.NewLiteral(recVals[n-i-1].Value, recVals[n-i-1].Type)
expr, err = expression.NewBinaryOperator(framework.Operator_BinaryGreaterThan).WithChildren(
ctx,
recExprs[n-i-1],
newLit,
)
if err != nil {
return nil, err
}
andExprs[n-i-1] = expr
orExprs[i] = gmsexpr.JoinAnd(andExprs...)
}
return gmsexpr.JoinOr(orExprs...), nil
}
+247
View File
@@ -0,0 +1,247 @@
// Copyright 2024 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 (
"strconv"
"strings"
"time"
"github.com/cockroachdb/apd/v3"
"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/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/go-mysql-server/sql/types"
"github.com/dolthub/vitess/go/vt/proto/query"
pgexprs "github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtransform "github.com/dolthub/doltgresql/server/transform"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// TypeSanitizer converts all GMS types into Doltgres types. Some places, such as parameter binding, will always default
// to GMS types, so by taking care of all conversions here, we can ensure that Doltgres only needs to worry about its
// own types.
func TypeSanitizer(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
// TODO: this probably should not be opaque, we should let the analyzer dig into subqueries and analyze them when
// it chooses. Doing all type transformations upfront like this masks bugs where certain tyupe conversion errors
// only manifest in a subquery
return pgtransform.NodeExprsWithNodeWithOpaque(ctx, node, func(ctx *sql.Context, n sql.Node, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
// This can be updated if we find more expressions that return GMS types.
// These should eventually be replaced with Doltgres-equivalents over time, rendering this function unnecessary.
switch expr := expr.(type) {
case *expression.GetField:
switch n := n.(type) {
case *plan.Project, *plan.Filter, *plan.GroupBy:
child := n.Children()[0]
// Some dolt_ tables do not have doltgres types for their columns, so we convert them here
if rt, ok := child.(*plan.ResolvedTable); ok && strings.HasPrefix(rt.Name(), "dolt_") {
// This is a projection on a table, so we can safely convert the type
if _, ok := expr.Type(ctx).(*pgtypes.DoltgresType); !ok {
return pgexprs.NewGMSCast(expr), transform.NewTree, nil
}
}
}
return expr, transform.SameTree, nil
case *expression.Literal:
// We want to leave limit literals alone, as they are expected to be GMS types when they appear in certain
// parts of the query (subqueries in particular)
// TODO: fix the limit and offset validation analysis to handle doltgres types
if _, isLimit := n.(*plan.Limit); isLimit {
break
}
if _, isOffset := n.(*plan.Offset); isOffset {
break
}
return typeSanitizerLiterals(ctx, expr)
case *expression.Not, *expression.And, *expression.Or, *expression.Like:
return pgexprs.NewGMSCast(expr), transform.NewTree, nil
case sql.FunctionExpression:
// Compiled functions are Doltgres functions. We're only concerned with GMS functions.
if _, ok := expr.(framework.Function); !ok {
// Some aggregation functions cannot be wrapped due to expectations in the analyzer, so we exclude them here.
switch expr.FunctionName() {
case "Count", "CountDistinct", "group_concat", "JSONObjectAgg", "Sum":
case "coalesce":
// Replace GMS Coalesce with a Doltgres-native implementation that uses
// Postgres type-resolution rules (FindCommonType) to infer the result type.
// GMS's Coalesce.Type() falls back to LongText when its arguments are
// DoltgresTypes because they don't satisfy GMS's IsNumber/IsText checks.
if _, isPgCoalesce := expr.(*pgexprs.PgCoalesce); !isPgCoalesce {
children := expr.Children()
allDoltgresTypes := true
for _, child := range children {
if _, ok := child.Type(ctx).(*pgtypes.DoltgresType); !ok {
allDoltgresTypes = false
break
}
}
if allDoltgresTypes {
pgCoalesce, err := pgexprs.NewPgCoalesce(ctx, children...)
if err != nil {
return nil, transform.NewTree, err
}
return pgCoalesce, transform.NewTree, nil
}
}
// Fall through to GMSCast if children aren't DoltgresTypes yet.
if _, ok := expr.Type(ctx).(*pgtypes.DoltgresType); !ok {
return pgexprs.NewGMSCast(expr), transform.NewTree, nil
}
default:
// Some GMS functions wrap Doltgres parameters, so we'll only handle those that return GMS types
if _, ok := expr.Type(ctx).(*pgtypes.DoltgresType); !ok {
return pgexprs.NewGMSCast(expr), transform.NewTree, nil
}
}
}
case *plan.ExistsSubquery:
return pgexprs.NewExplicitCast(pgexprs.NewGMSCast(expr), pgtypes.Bool), transform.NewTree, nil
case *sql.ColumnDefaultValue:
// Due to how interfaces work, we sometimes pass (*ColumnDefaultValue)(nil), so we have to check for it
if expr != nil && expr.Expr != nil {
defaultExpr := expr.Expr
if _, ok := defaultExpr.Type(ctx).(*pgtypes.DoltgresType); !ok {
defaultExpr = pgexprs.NewGMSCast(defaultExpr)
}
defaultExprType := defaultExpr.Type(ctx).(*pgtypes.DoltgresType)
outType, ok := expr.OutType.(*pgtypes.DoltgresType)
if !ok {
return nil, transform.NewTree, errors.Errorf("default values must have a non-GMS OutType: `%s`", expr.OutType.String())
}
if !outType.Equals(defaultExprType) {
defaultExpr = pgexprs.NewAssignmentCast(defaultExpr, defaultExprType, outType)
}
newDefault, err := sql.NewColumnDefaultValue(defaultExpr, outType, expr.Literal, expr.Parenthesized, expr.ReturnNil)
return newDefault, transform.NewTree, err
}
}
return expr, transform.SameTree, nil
})
}
// typeSanitizerLiterals handles literal expressions for TypeSanitizer.
func typeSanitizerLiterals(ctx *sql.Context, gmsLiteral *expression.Literal) (sql.Expression, transform.TreeIdentity, error) {
// GMS may resolve Doltgres literals and then stick them in GMS literals, so we have to account for that here
if doltgresType, ok := gmsLiteral.Type(ctx).(*pgtypes.DoltgresType); ok {
return pgexprs.NewUnsafeLiteral(gmsLiteral.Value(), doltgresType), transform.NewTree, nil
}
switch gmsLiteral.Type(ctx).Type() {
case query.Type_INT8, query.Type_INT16, query.Type_YEAR, query.Type_INT24, query.Type_INT32:
newVal, _, err := types.Int32.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
return pgexprs.NewRawLiteralInt32(newVal.(int32)), transform.NewTree, nil
case query.Type_INT64, query.Type_ENUM:
newVal, _, err := types.Int64.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
return pgexprs.NewRawLiteralInt64(newVal.(int64)), transform.NewTree, nil
case query.Type_UINT8, query.Type_UINT16, query.Type_UINT24, query.Type_UINT32:
newVal, _, err := types.Uint32.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
return pgexprs.NewRawLiteralInt64(int64(newVal.(uint32))), transform.NewTree, nil
case query.Type_UINT64, query.Type_SET:
newVal, _, err := types.Uint64.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
newLiteral, err := pgexprs.NewNumericLiteral(strconv.FormatUint(newVal.(uint64), 10))
return newLiteral, transform.NewTree, err
case query.Type_FLOAT32:
newVal, _, err := types.Float32.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
return pgexprs.NewRawLiteralFloat32(newVal.(float32)), transform.NewTree, nil
case query.Type_FLOAT64:
newVal, _, err := types.Float64.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
return pgexprs.NewRawLiteralFloat64(newVal.(float64)), transform.NewTree, nil
case query.Type_DECIMAL:
dec, ok := gmsLiteral.Value().(*apd.Decimal)
if !ok {
return nil, transform.NewTree, errors.Errorf("SANITIZER: expected decimal type: %T", gmsLiteral.Value())
}
return pgexprs.NewRawLiteralNumeric(dec), transform.NewTree, nil
case query.Type_DATE, query.Type_DATETIME, query.Type_TIMESTAMP:
newVal, _, err := types.Datetime.Convert(ctx, gmsLiteral.Value())
if err != nil {
return nil, transform.NewTree, err
}
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
return pgexprs.NewRawLiteralTimestamp(newVal.(time.Time)), transform.NewTree, nil
case query.Type_CHAR, query.Type_VARCHAR, query.Type_TEXT:
str, ok := gmsLiteral.Value().(string)
if !ok {
return nil, transform.NewTree, errors.Errorf("SANITIZER: expected string type: %T", gmsLiteral.Value())
}
return pgexprs.NewUnknownLiteral(str), transform.NewTree, nil
case query.Type_BINARY, query.Type_VARBINARY, query.Type_BLOB:
newVal := gmsLiteral.Value()
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
} else if str, ok := newVal.(string); ok {
return pgexprs.NewUnknownLiteral(str), transform.NewTree, nil
} else if b, ok := newVal.([]byte); ok {
return pgexprs.NewUnknownLiteral(string(b)), transform.NewTree, nil
}
return nil, transform.NewTree, errors.Errorf("SANITIZER: invalid binary type: %T", gmsLiteral.Value())
case query.Type_JSON:
newVal := gmsLiteral.Value()
if newVal == nil {
return pgexprs.NewNullLiteral(), transform.NewTree, nil
}
str, ok := newVal.(string)
if !ok {
return nil, transform.NewTree, errors.Errorf("SANITIZER: expected string type: %T", gmsLiteral.Value())
}
return pgexprs.NewUnknownLiteral(str), transform.NewTree, nil
case query.Type_NULL_TYPE:
return pgexprs.NewNullLiteral(), transform.NewTree, nil
default:
return nil, transform.NewTree, errors.Errorf("SANITIZER: encountered a GMS type that cannot be handled: %s", gmsLiteral.Type(ctx).String())
}
}
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2020-2021 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/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
pgnode "github.com/dolthub/doltgresql/server/node"
)
// ValidateColumnDefaults ensures that newly created column defaults from a DDL statement are legal for the type of
// column, various other business logic checks to match MySQL's logic.
func ValidateColumnDefaults(ctx *sql.Context, _ *analyzer.Analyzer, n sql.Node, _ *plan.Scope, _ analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
span, ctx := ctx.Span("validateColumnDefaults")
defer span.End()
return transform.Node(ctx, n, func(ctx *sql.Context, n sql.Node) (sql.Node, transform.TreeIdentity, error) {
switch node := n.(type) {
case *plan.AlterDefaultSet:
table := getResolvedTable(node)
sch := table.Schema(ctx)
index := sch.IndexOfColName(node.ColumnName)
if index == -1 {
return nil, transform.SameTree, sql.ErrColumnNotFound.New(node.ColumnName)
}
col := sch[index]
err := validateColumnDefault(ctx, col, node.Default)
if err != nil {
return node, transform.SameTree, err
}
return node, transform.SameTree, nil
case sql.SchemaTarget:
switch node.(type) {
case *plan.AlterPK, *plan.AddColumn, *plan.ModifyColumn, *plan.AlterDefaultDrop, *plan.CreateTable, *plan.DropColumn, *pgnode.CreateTable:
// DDL nodes must validate any new column defaults, continue to logic below
default:
// other node types are not altering the schema and therefore don't need validation of column defaults
return n, transform.SameTree, nil
}
// There may be multiple DDL nodes in the plan (ALTER TABLE statements can have many clauses), and for each of them
// we need to count the column indexes in the very hacky way outlined above.
i := 0
return transform.NodeExprs(ctx, n, func(ctx *sql.Context, e sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
eWrapper, ok := e.(*expression.Wrapper)
if !ok {
return e, transform.SameTree, nil
}
defer func() {
i++
}()
eVal := eWrapper.Unwrap()
if eVal == nil {
return e, transform.SameTree, nil
}
colDefault, ok := eVal.(*sql.ColumnDefaultValue)
if !ok {
return e, transform.SameTree, nil
}
col, err := lookupColumnForTargetSchema(ctx, node, i)
if err != nil {
return nil, transform.SameTree, err
}
err = validateColumnDefault(ctx, col, colDefault)
if err != nil {
return nil, transform.SameTree, err
}
return e, transform.SameTree, nil
})
default:
return node, transform.SameTree, nil
}
})
}
// lookupColumnForTargetSchema looks at the target schema for the specified SchemaTarget node and returns
// the column based on the specified index. For most node types, this is simply indexing into the target
// schema but a few types require special handling.
func lookupColumnForTargetSchema(_ *sql.Context, node sql.SchemaTarget, colIndex int) (*sql.Column, error) {
schema := node.TargetSchema()
switch n := node.(type) {
case *plan.ModifyColumn:
if colIndex < len(schema) {
return schema[colIndex], nil
} else {
return n.NewColumn(), nil
}
case *plan.AddColumn:
if colIndex < len(schema) {
return schema[colIndex], nil
} else {
return n.Column(), nil
}
case *plan.AlterDefaultSet:
index := schema.IndexOfColName(n.ColumnName)
if index == -1 {
return nil, sql.ErrTableColumnNotFound.New(n.Table, n.ColumnName)
}
return schema[index], nil
default:
if colIndex < len(schema) {
return schema[colIndex], nil
} else {
// TODO: sql.ErrColumnNotFound would be a better error here, but we need to add all the different node types to
// the switch to get it
return nil, expression.ErrIndexOutOfBounds.New(colIndex, len(schema))
}
}
}
// validateColumnDefault validates that the column default expression is valid for the column type and returns an error
// if not
func validateColumnDefault(ctx *sql.Context, col *sql.Column, colDefault *sql.ColumnDefaultValue) error {
if colDefault == nil {
return nil
}
var err error
sql.Inspect(ctx, colDefault.Expr, func(ctx *sql.Context, e sql.Expression) bool {
switch e.(type) {
case sql.FunctionExpression, *expression.UnresolvedFunction:
// TODO: functions must be deterministic to be used in column defaults
return true
case *plan.Subquery:
err = sql.ErrColumnDefaultSubquery.New(col.Name)
return false
case *expression.GetField:
if !colDefault.IsParenthesized() {
err = sql.ErrInvalidColumnDefaultValue.New(col.Name)
return false
}
return true
default:
return true
}
})
if err != nil {
return err
}
// validate type of default expression
if err = colDefault.CheckType(ctx); err != nil {
return err
}
return nil
}
// Finds first ResolvedTable node that is a descendant of the node given
// This function will not look inside SubqueryAliases
func getResolvedTable(node sql.Node) *plan.ResolvedTable {
var table *plan.ResolvedTable
transform.Inspect(node, func(n sql.Node) bool {
// Inspect is called on all children of a node even if an earlier child's call returns false.
// We only want the first TableNode match.
if table != nil {
return false
}
switch nn := n.(type) {
case *plan.SubqueryAlias:
// We should not be matching with ResolvedTables inside SubqueryAliases
return false
case *plan.ResolvedTable:
if !plan.IsDualTable(nn) {
table = nn
return false
}
case *plan.IndexedTableAccess:
if rt, ok := nn.TableNode.(*plan.ResolvedTable); ok {
table = rt
return false
}
}
return true
})
return table
}
+54
View File
@@ -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"
"github.com/dolthub/doltgresql/core"
)
// ValidateCreateSchema validates that CREATE SCHEMA is executed with a valid database context.
// In PostgreSQL, schemas exist within databases, so CREATE SCHEMA requires an active database.
// See: https://github.com/dolthub/doltgresql/issues/1863
func ValidateCreateSchema(
ctx *sql.Context,
a *analyzer.Analyzer,
n sql.Node,
scope *plan.Scope,
sel analyzer.RuleSelector,
qFlags *sql.QueryFlags,
) (sql.Node, transform.TreeIdentity, error) {
cs, ok := n.(*plan.CreateSchema)
if !ok {
return n, transform.SameTree, nil
}
// Check if the current database actually has a working root.
// GetRootFromContext will return sql.ErrDatabaseNotFound if the database
// is not properly initialized.
_, _, err := core.GetRootFromContext(ctx)
if err != nil {
if sql.ErrDatabaseNotFound.Is(err) {
return nil, transform.SameTree, sql.ErrNoDatabaseSelected.New()
}
return nil, transform.SameTree, err
}
return cs, transform.SameTree, nil
}
+462
View File
@@ -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
}