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