Files
2026-07-13 12:32:25 +08:00

210 lines
7.4 KiB
Go

// 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
}