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