174 lines
6.1 KiB
Go
174 lines
6.1 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 expranalysis
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/dolthub/go-mysql-server/memory"
|
|
"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/dolt/go/libraries/doltcore/schema"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/overrides"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlfmt"
|
|
)
|
|
|
|
// ResolveDefaultExpression returns a sql.Expression for the column default or generated expression for the
|
|
// column provided
|
|
func ResolveDefaultExpression(ctx *sql.Context, tableName string, sch schema.Schema, col schema.Column) (sql.Expression, error) {
|
|
ct, err := parseCreateTable(ctx, tableName, sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
colIdx := ct.PkSchema().Schema.IndexOfColName(col.Name)
|
|
if colIdx < 0 {
|
|
return nil, fmt.Errorf("unable to find column %s in analyzed query", col.Name)
|
|
}
|
|
|
|
sqlCol := ct.PkSchema().Schema[colIdx]
|
|
expr := sqlCol.Default
|
|
if expr == nil || expr.Expr == nil {
|
|
expr = sqlCol.Generated
|
|
}
|
|
|
|
if expr == nil || expr.Expr == nil {
|
|
return nil, fmt.Errorf("unable to find default or generated expression")
|
|
}
|
|
|
|
return expr, nil
|
|
}
|
|
|
|
// ResolveCheckExpression returns a sql.Expression for the check provided
|
|
func ResolveCheckExpression(ctx *sql.Context, tableName string, sch schema.Schema, checkExpr string) (sql.Expression, error) {
|
|
ct, err := parseCreateTable(ctx, tableName, sch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
formatter := overrides.SchemaFormatterFromContext(ctx)
|
|
for _, check := range ct.Checks() {
|
|
// Check definitions created before v1.55.3 may not have backquotes around identifiers
|
|
quotedExpr := stripTableNamesFromExpression(ctx, formatter, check.Expr, true).String()
|
|
unquotedExpr := stripTableNamesFromExpression(ctx, formatter, check.Expr, false).String()
|
|
if quotedExpr == checkExpr || unquotedExpr == checkExpr {
|
|
return check.Expr, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("unable to find check expression")
|
|
}
|
|
|
|
// ResolveExpression compiles a predicate or check constraint string into a sql.Expression resolved.
|
|
// Used to re-hydrate partial index predicates and check expressions from their string representation.
|
|
// It uses SELECT statement on the expression FROM given table.
|
|
func ResolveExpression(ctx *sql.Context, tableName string, predicateStr string) (sql.Expression, error) {
|
|
// TODO: added * in SELECT stmt to avoid pruning columns during analyzer.
|
|
parsed, err := parseQuery(ctx, fmt.Sprintf("SELECT *, %s FROM %s", predicateStr, tableName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sess, ok := ctx.Session.(SessionDbProvider)
|
|
if ok {
|
|
// field index in GetField is incorrect, it needs `fix_exec_indexes` analyzer rule
|
|
a := analyzer.NewDefault(sess.GenericProvider())
|
|
parsed, err = a.Analyze(ctx, parsed, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// comes out in ContextRootFinalizer, so unwrap it
|
|
parsed = parsed.Children()[0]
|
|
}
|
|
|
|
proj, ok := parsed.(*plan.Project)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected *plan.Project parsing predicate, got %T", parsed)
|
|
}
|
|
expr := proj.Projections[len(proj.Projections)-1]
|
|
if alias, ok := expr.(*expression.Alias); ok {
|
|
expr = alias.Child
|
|
}
|
|
return expr, nil
|
|
}
|
|
|
|
func stripTableNamesFromExpression(ctx *sql.Context, formatter sql.SchemaFormatter, expr sql.Expression, quoted bool) sql.Expression {
|
|
e, _, _ := transform.Expr(ctx, expr, func(ctx *sql.Context, e sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
|
|
if col, ok := e.(*expression.GetField); ok {
|
|
return col.WithTable("").WithQuotedNames(formatter, quoted), transform.NewTree, nil
|
|
}
|
|
return e, transform.SameTree, nil
|
|
})
|
|
return e
|
|
}
|
|
|
|
// SessionDbProvider is a way to access the Dolt database provider without import cycles.
|
|
type SessionDbProvider interface {
|
|
GenericProvider() sql.MutableDatabaseProvider
|
|
}
|
|
|
|
func parseCreateTable(ctx *sql.Context, tableName string, sch schema.Schema) (*plan.CreateTable, error) {
|
|
createTable, err := sqlfmt.GenerateCreateTableStatement(ctx, tableName, sch, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pseudoAnalyzedQuery, err := parseQuery(ctx, createTable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ct, ok := pseudoAnalyzedQuery.(*plan.CreateTable)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected a *plan.CreateTable node, but got %T", pseudoAnalyzedQuery)
|
|
}
|
|
return ct, nil
|
|
}
|
|
|
|
func parseQuery(ctx *sql.Context, query string) (sql.Node, error) {
|
|
// Doltgres must use the existing provider due to custom functions, so we split the logic here depending on whether
|
|
// this is being called from a query context (which will have a valid Dolt session) or some other context (which
|
|
// will need to construct a new provider).
|
|
var b *planbuilder.Builder
|
|
engOverrides := overrides.EngineOverridesFromContext(ctx)
|
|
sess, ok := ctx.Session.(SessionDbProvider)
|
|
if ok {
|
|
catalog := analyzer.NewCatalog(sess.GenericProvider(), engOverrides)
|
|
catalog.AuthHandler = sql.NoopAuthorizationHandler{}
|
|
b = planbuilder.New(ctx, catalog, nil)
|
|
} else {
|
|
mockDatabase := memory.NewDatabase("mydb")
|
|
mockProvider := memory.NewDBProvider(mockDatabase)
|
|
catalog := analyzer.NewCatalog(mockProvider, engOverrides)
|
|
catalog.AuthHandler = sql.NoopAuthorizationHandler{}
|
|
// We need a new context for this operation
|
|
parseCtx := sql.NewEmptyContext()
|
|
parseCtx.SetCurrentDatabase("mydb")
|
|
|
|
b = planbuilder.New(parseCtx, catalog, nil)
|
|
}
|
|
|
|
pseudoAnalyzedQuery, _, _, _, err := b.Parse(query, nil, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pseudoAnalyzedQuery, nil
|
|
}
|