201 lines
9.5 KiB
Go
201 lines
9.5 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 (
|
|
"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
|
|
}
|