210 lines
7.4 KiB
Go
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
|
|
}
|