// Copyright © 2008, 2015, Oracle and/or its affiliates. All rights reserved. // // MySQL Connector/NET is licensed under the terms of the GPLv2 // , like most // MySQL Connectors. There are special exceptions to the terms and // conditions of the GPLv2 as it is applied to this software, see the // FLOSS License Exception // . // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 2 of the License. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA using System; using System.Diagnostics; using System.Text; using System.Data; using System.Collections.Generic; using MySql.Data.MySqlClient; using System.Globalization; #if EF6 using System.Data.Entity.Core.Common.CommandTrees; using System.Data.Entity.Core.Metadata.Edm; #else using System.Data.Common.CommandTrees; using System.Data.Metadata.Edm; #endif namespace MySql.Data.Entity { abstract class SqlGenerator : DbExpressionVisitor { protected string tabs = String.Empty; private int parameterCount = 1; protected Scope scope = new Scope(); protected int propertyLevel; protected Dictionary values; protected internal Stack Ops = new Stack(); public SqlGenerator() { Parameters = new List(); } protected internal OpType GetTopOp() { if (Ops.Count == 0) return OpType.Join; else return Ops.Peek(); } #region Properties public List Parameters { get; private set; } // protected SymbolTable Symbols { get; private set; } #endregion public virtual string GenerateSQL(DbCommandTree commandTree) { throw new NotImplementedException(); } protected string CreateUniqueParameterName() { return String.Format("@gp{0}", parameterCount++); } #region DbExpressionVisitor Base Implementations public override SqlFragment Visit(DbVariableReferenceExpression expression) { PropertyFragment fragment = new PropertyFragment(); fragment.Properties.Add(expression.VariableName); return fragment; } public override SqlFragment Visit(DbPropertyExpression expression) { propertyLevel++; PropertyFragment fragment = expression.Instance.Accept(this) as PropertyFragment; fragment.Properties.Add(expression.Property.Name); propertyLevel--; // if we are not at the top level property then just return if (propertyLevel > 0) return fragment; ColumnFragment column = new ColumnFragment(null, fragment.LastProperty); column.PropertyFragment = fragment; InputFragment input = scope.FindInputFromProperties(fragment); if (input != null) column.TableName = input.Name; // now we need to check if our column name was possibly renamed if (input is TableFragment ) { if (!string.IsNullOrEmpty(input.Name)) { SelectStatement sf = scope.GetFragment(input.Name) as SelectStatement; if (sf != null) { // Special case: undo alias in case of query fusing for (int i = 0; i < sf.Columns.Count; i++) { ColumnFragment cf = sf.Columns[i]; if (column.ColumnName == cf.ColumnAlias) { column.ColumnName = cf.ColumnName; column.ColumnAlias = cf.ColumnAlias; column.TableName = input.Name; return column; } } } } return column; } SelectStatement select = input as SelectStatement; UnionFragment union = input as UnionFragment; if (select != null) select.HasDifferentNameForColumn(column); else if (union != null) union.HasDifferentNameForColumn(column); // input is a table, selectstatement, or unionstatement return column; } public override SqlFragment Visit(DbScanExpression expression) { EntitySetBase target = expression.Target; TableFragment fragment = new TableFragment(); MetadataProperty property; bool propExists = target.MetadataProperties.TryGetValue("DefiningQuery", true, out property); if (propExists && property.Value != null) { MetadataProperty prop2; if( target.MetadataProperties.TryGetValue( "http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator:Type", true, out prop2 ) && ( prop2.Value as string == "Views" )) { // avoid storing view query as DefiningQuery because that hurts query fusing. fragment.Schema = target.MetadataProperties.GetValue("http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator:Schema", true).Value as string; fragment.Table = target.Name; } else { fragment.DefiningQuery = new LiteralFragment(property.Value as string); } } else { fragment.Schema = target.EntityContainer.Name; fragment.Table = target.Name; propExists = target.MetadataProperties.TryGetValue("Schema", true, out property); if (propExists && property.Value != null) fragment.Schema = property.Value as string; propExists = target.MetadataProperties.TryGetValue("Table", true, out property); if (propExists && property.Value != null) fragment.Table = property.Value as string; } return fragment; } public override SqlFragment Visit(DbParameterReferenceExpression expression) { return new LiteralFragment("@" + expression.ParameterName); } public override SqlFragment Visit(DbNotExpression expression) { SqlFragment f = expression.Argument.Accept(this); Debug.Assert(f is NegatableFragment); NegatableFragment nf = f as NegatableFragment; nf.Negate(); return nf; } public override SqlFragment Visit(DbIsEmptyExpression expression) { ExistsFragment f = new ExistsFragment(expression.Argument.Accept(this)); f.Negate(); return f; } public override SqlFragment Visit(DbFunctionExpression expression) { FunctionProcessor gen = new FunctionProcessor(); return gen.Generate(expression, this); } public override SqlFragment Visit(DbConstantExpression expression) { PrimitiveTypeKind pt = ((PrimitiveType)expression.ResultType.EdmType).PrimitiveTypeKind; string literal = Metadata.GetNumericLiteral(pt, expression.Value); if (literal != null) return new LiteralFragment(literal); else if (pt == PrimitiveTypeKind.Boolean) return new LiteralFragment((bool)expression.Value ? "1" : "0"); else { // use a parameter for non-numeric types so we get proper // quoting MySqlParameter p = new MySqlParameter(); p.ParameterName = CreateUniqueParameterName(); p.DbType = Metadata.GetDbType(expression.ResultType); p.Value = Metadata.NormalizeValue(expression.ResultType, expression.Value); Parameters.Add(p); return new LiteralFragment(p.ParameterName); } } public override SqlFragment Visit(DbComparisonExpression expression) { return VisitBinaryExpression(expression.Left, expression.Right, Metadata.GetOperator(expression.ExpressionKind)); } public override SqlFragment Visit(DbAndExpression expression) { return VisitBinaryExpression(expression.Left, expression.Right, "AND"); } public override SqlFragment Visit(DbOrExpression expression) { return VisitBinaryExpression(expression.Left, expression.Right, "OR"); } public override SqlFragment Visit(DbCastExpression expression) { //TODO: handle casting return expression.Argument.Accept(this); } #if EF6 public override SqlFragment Visit(DbInExpression expression) { SqlFragment sf = expression.Item.Accept(this); InFragment inf = new InFragment(); inf.Argument = sf; for( int i = 0; i < expression.List.Count; i++ ) { LiteralFragment lf = Visit( expression.List[ i ] as DbConstantExpression ) as LiteralFragment; inf.InList.Add( lf ); } return inf; } public override SqlFragment Visit(DbLambdaExpression expression) { throw new NotImplementedException(); } #endif public override SqlFragment Visit(DbLikeExpression expression) { LikeFragment f = new LikeFragment(); f.Argument = expression.Argument.Accept(this); f.Pattern = expression.Pattern.Accept(this); return f; } public override SqlFragment Visit(DbCaseExpression expression) { CaseFragment c = new CaseFragment(); Debug.Assert(expression.When.Count == expression.Then.Count); for (int i = 0; i < expression.When.Count; ++i) { c.When.Add(expression.When[i].Accept(this)); c.Then.Add(expression.Then[i].Accept(this)); } if (expression.Else != null && !(expression.Else is DbNullExpression)) c.Else = expression.Else.Accept(this); return c; } public override SqlFragment Visit(DbIsNullExpression expression) { IsNullFragment f = new IsNullFragment(); f.Argument = expression.Argument.Accept(this); return f; } public override SqlFragment Visit(DbIntersectExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbNullExpression expression) { return new LiteralFragment("NULL"); } public override SqlFragment Visit(DbArithmeticExpression expression) { if (expression.ExpressionKind == DbExpressionKind.UnaryMinus) { ListFragment f = new ListFragment(); f.Append("-("); f.Append(expression.Arguments[0].Accept(this)); f.Append(")"); return f; } string op = String.Empty; switch (expression.ExpressionKind) { case DbExpressionKind.Divide: op = "/"; break; case DbExpressionKind.Minus: op = "-"; break; case DbExpressionKind.Modulo: op = "%"; break; case DbExpressionKind.Multiply: op = "*"; break; case DbExpressionKind.Plus: op = "+"; break; default: throw new NotSupportedException(); } return VisitBinaryExpression(expression.Arguments[0], expression.Arguments[1], op); } protected void VisitNewInstanceExpression(SelectStatement select, DbNewInstanceExpression expression) { Debug.Assert(expression.ResultType.EdmType is RowType); RowType row = expression.ResultType.EdmType as RowType; for (int i = 0; i < expression.Arguments.Count; i++) { ColumnFragment col; SqlFragment fragment = expression.Arguments[i].Accept(this); if (fragment is ColumnFragment) col = fragment as ColumnFragment; else { col = new ColumnFragment(null, null); col.Literal = fragment; } col.ColumnAlias = row.Properties[i].Name; select.Columns.Add(col); } } public override SqlFragment Visit(DbTreatExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbRelationshipNavigationExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbRefExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbOfTypeExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbIsOfExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbRefKeyExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbEntityRefExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbExceptExpression expression) { throw new NotSupportedException(); } public override SqlFragment Visit(DbExpression expression) { throw new InvalidOperationException(); } public override SqlFragment Visit(DbDerefExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbApplyExpression expression) { throw new NotSupportedException(); } #endregion #region DBExpressionVisitor methods normally overridden public override SqlFragment Visit(DbUnionAllExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbSortExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbSkipExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbQuantifierExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbProjectExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbNewInstanceExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbLimitExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbJoinExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbGroupByExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbFilterExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbElementExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbDistinctExpression expression) { throw new NotImplementedException(); } public override SqlFragment Visit(DbCrossJoinExpression expression) { throw new NotImplementedException(); } #endregion #region "Optimization" /// /// If current fragment is select and its from clause is another select, try fuse the inner select with the outer select. /// (Thus removing a nested query, which may have bad performance in Mysql). /// /// The fragment to probe and posibly optimize /// The fragment fused, or the original one. protected internal InputFragment TryFusingSelect(InputFragment f) { SelectStatement result = f as SelectStatement; if (!CanFuseSelect(f as SelectStatement)) return f; result = FuseSelectWithInnerSelect(result, result.From as SelectStatement); return result; } protected internal SelectStatement FuseSelectWithInnerSelect(SelectStatement outer, SelectStatement inner) { string oldTableName = (inner.From as TableFragment).Name; string newTableName = inner.Name; Dictionary dicColumns = new Dictionary(); foreach (ColumnFragment cf in inner.Columns) { if (cf.ColumnAlias != null) dicColumns.Add(cf.ColumnAlias, cf); } outer.From = inner.From; (outer.From as TableFragment).Name = newTableName; // Dispatch Where if (outer.Where == null) { outer.Where = inner.Where; } else if (inner.Where != null) { outer.Where = new BinaryFragment() { Left = outer.Where, Right = inner.Where, Operator = "AND" }; } VisitAndReplaceTableName(outer.Where, oldTableName, newTableName, dicColumns); // For the next constructions, either is defined on outer or at inner, not both // Dispatch Limit if (outer.Limit == null) { outer.Limit = inner.Limit; VisitAndReplaceTableName(outer.Limit, oldTableName, newTableName, dicColumns); } // Dispatch GroupBy if (outer.GroupBy == null && inner.GroupBy != null) { foreach (SqlFragment sf in inner.GroupBy) outer.AddGroupBy(sf); foreach (SqlFragment sf in outer.GroupBy) VisitAndReplaceTableName(sf, oldTableName, newTableName, dicColumns); } // Dispatch OrderBy if (outer.OrderBy != null || inner.OrderBy != null) { if (inner.OrderBy != null) { foreach (SortFragment sf in inner.OrderBy) outer.AddOrderBy(sf); } foreach (SortFragment sf in outer.OrderBy) VisitAndReplaceTableName(sf, oldTableName, newTableName, dicColumns); } // Dispatch Skip if (outer.Skip == null) outer.Skip = inner.Skip; return outer; } protected internal bool CanFuseSelect(SelectStatement select) { SelectStatement innerSelect = null; if (select == null || select.Columns.Count != 0) return false; innerSelect = select.From as SelectStatement; if ((innerSelect == null) || !( innerSelect.From is TableFragment )) return false; // Cannot fuse, unless construction is semantically compatible // ie. Where's can be combined, Group by's no. if ((select.Limit == null || innerSelect.Limit == null) && (select.GroupBy == null || innerSelect.GroupBy == null) && (select.OrderBy == null || innerSelect.OrderBy == null) && (select.Skip == null || innerSelect.Skip == null) && ( select.IsDistinct == innerSelect.IsDistinct ) ) { List cols = innerSelect.Columns; for (int i = 0; i < cols.Count; i++) if (cols[i].Literal != null) return false; return true; } else { return false; } } protected internal void VisitAndReplaceTableName(SqlFragment sf, string oldTable, string newTable, Dictionary dicColumns) { if (sf == null) return; ReplaceTableNameVisitor visitor = new ReplaceTableNameVisitor(oldTable, newTable, dicColumns); sf.Accept(visitor); } #endregion protected InputFragment VisitInputExpression(DbExpression e, string name, TypeUsage type) { SqlFragment f = e.Accept(this); Debug.Assert(f is InputFragment); InputFragment inputFragment = f as InputFragment; inputFragment.Name = name; TryFusingSelect(inputFragment); if (inputFragment is TableFragment && type != null) (inputFragment as TableFragment).Type = type; SelectStatement select = inputFragment as SelectStatement; if ((select != null) && (select.From is TableFragment)) { (select.From as TableFragment).Type = type; } if (name != null) scope.Add(name, inputFragment); return inputFragment; } protected virtual SelectStatement GenerateReturningSql(DbModificationCommandTree tree, DbExpression returning) { SelectStatement select = new SelectStatement(this); Debug.Assert(returning is DbNewInstanceExpression); VisitNewInstanceExpression(select, returning as DbNewInstanceExpression); select.From = (InputFragment)tree.Target.Expression.Accept(this); ListFragment where = new ListFragment(); select.Where = where; return select; } #region Private Methods /// /// Examines a binary expression to see if it is an special case suitable to conversion /// to a more efficient and equivalent LIKE sql expression. /// /// /// /// /// protected LikeFragment TryPromoteToLike(DbExpression left, DbExpression right, string op) { DbFunctionExpression fl = left as DbFunctionExpression; if ((fl != null) && (right is DbConstantExpression)) { LikeFragment like = new LikeFragment(); if (fl.Function.FullName == "Edm.IndexOf") { DbParameterReferenceExpression par; DbPropertyExpression prop; int value = Convert.ToInt32(((DbConstantExpression)right).Value); like.Argument = fl.Arguments[1].Accept(this); if ((value == 1) && (op == "=")) { DbFunctionExpression fr1; DbFunctionExpression fr2; if ((fl.Arguments[0] is DbConstantExpression)) { // Case LIKE 'pattern%' DbConstantExpression c = (DbConstantExpression)fl.Arguments[0]; like.Pattern = new LiteralFragment(string.Format("'{0}%'", c.Value.ToString().Replace( "'", "''" ) )); return like; } else if ((fl.Arguments.Count == 2) && ((fr1 = fl.Arguments[0] as DbFunctionExpression) != null) && ((fr2 = fl.Arguments[1] as DbFunctionExpression) != null) && (fr1.Function.FullName == "Edm.Reverse") && (fr2.Function.FullName == "Edm.Reverse")) { // Case LIKE '%pattern' in EF .NET 4.0 if (fr1.Arguments[0] is DbConstantExpression) { DbConstantExpression c = (DbConstantExpression)fr1.Arguments[0]; like.Pattern = new LiteralFragment(string.Format("'%{0}'", c.Value.ToString().Replace("'", "''") )); like.Argument = fr2.Arguments[0].Accept(this); return like; } else if( /* For EF6 */ (( par = fr1.Arguments[0] as DbParameterReferenceExpression ) != null ) && (( prop = fr2.Arguments[0] as DbPropertyExpression) != null )) { // Pattern LIKE "%..." in EF6 like.Pattern = par.Accept(this); like.Argument = prop.Accept(this); return like; } } else if( ( fl.Arguments.Count == 2) && (( par = fl.Arguments[0] as DbParameterReferenceExpression ) != null ) && (( prop = fl.Arguments[1] as DbPropertyExpression ) != null ) ) { // Case LIKE "pattern%" in EF6 like.Pattern = par.Accept(this); like.Argument = prop.Accept(this); return like; } } else if (value == 0) { if ( op == ">") { if (fl.Arguments[0] is DbConstantExpression) { // Case LIKE '%pattern%' DbConstantExpression c = (DbConstantExpression)fl.Arguments[0]; like.Pattern = new LiteralFragment(string.Format("%{0}%", c.Value.ToString())); return like; } else if ((par = fl.Arguments[0] as DbParameterReferenceExpression) != null) { // Case LIKE "%pattern%" in EF6 like.Pattern = fl.Arguments[0].Accept(this); return like; } } } } // Like '%pattern' in EF .NET 3.5 (yes, is different than in .NET 4.0) else if (fl.Function.FullName == "Edm.Right") { DbFunctionExpression fLength = fl.Arguments[1] as DbFunctionExpression; if ((fLength != null) && (fLength.Function.FullName == "Edm.Length") && (fLength.Arguments[0] is DbConstantExpression)) { DbConstantExpression c2 = fLength.Arguments[0] as DbConstantExpression; DbConstantExpression c1 = (DbConstantExpression)right; if (c1.Value == c2.Value) { like.Argument = fl.Arguments[0].Accept(this); like.Pattern = new LiteralFragment(string.Format("'%{0}'", c1.Value.ToString().Replace("'", "''") )); return like; } } } } return null; } protected virtual SqlFragment VisitBinaryExpression(DbExpression left, DbExpression right, string op) { // Optimization: try to use 'like' instead of 'locate' (Edm.IndexOf) for these // cases: (like 'word%'), (like '%word') & (like '%word%'). LikeFragment like = TryPromoteToLike(left, right, op); if (like != null) return like; // normal flow BinaryFragment f = new BinaryFragment(); f.Operator = op; f.Left = left.Accept(this); f.WrapLeft = ShouldWrapExpression(left); f.Right = right.Accept(this); f.WrapRight = ShouldWrapExpression(right); // Optimization, try to promote to In expression // NOTE: In EF6, this optimization is already done, we just implement Visit(DbInExpression). #if !EF6 return TryToPromoteToIn(f); #else return f; #endif } protected virtual SqlFragment TryToPromoteToIn(BinaryFragment bf) { // TODO: Remember Morgan's theorem // Only try to merge if they are OR'ed. if ((bf.Operator == "OR") ) { InFragment inf = bf.Left as InFragment; InFragment inf2 = bf.Right as InFragment; if (inf == null) { BinaryFragment bfLeft = bf.Left as BinaryFragment; if( bfLeft == null ) return bf; if (inf2 == null) { // try to create a new infragment BinaryFragment bfRight = bf.Right as BinaryFragment; if (bfRight == null) return bf; InFragment inff = TryMergeTwoBinaryFragments(bfLeft, bfRight); if (inff != null) return inff; } else { // try to merge an existing infragment & a binaryfragment. SqlFragment sf = TryMergeBinaryFragmentAndInFragment(bfLeft, inf2); if (sf != null) return sf; } } else if (inf2 == null) { BinaryFragment bfRight = bf.Right as BinaryFragment; if (bfRight == null) return bf; else { // try to merge an existing infragment & a binaryfragment. SqlFragment sf = TryMergeBinaryFragmentAndInFragment(bfRight, inf); if (sf != null) return sf; } } else { // try to merge both InFragments SqlFragment sf = TryMergeTwoInFragments( inf, inf2 ); if (sf != null) return sf; } } return bf; } protected InFragment TryMergeTwoInFragments(InFragment infLeft, InFragment infRight) { if (infLeft.Argument.Equals(infRight.Argument) && ( infLeft.IsNegated == infRight.IsNegated ) && ( !infLeft.IsNegated ) ) { infLeft.InList.AddRange(infRight.InList); return infLeft; } return null; } protected InFragment TryMergeTwoBinaryFragments(BinaryFragment left, BinaryFragment right) { if( ( left.IsNegated == right.IsNegated ) && ( ! left.IsNegated ) && ( left.Operator == "=" ) && ( right.Operator == "=" ) ) { ColumnFragment cf; LiteralFragment lf; GetBinaryFragmentPartsForIn(left, out lf, out cf); if ((lf != null) && (cf != null)) { ColumnFragment cf2; LiteralFragment lf2; GetBinaryFragmentPartsForIn(right, out lf2, out cf2); if ( (lf2 != null) && (cf2 != null) && cf.Equals(cf2)) { InFragment inf = new InFragment(); inf.Argument = cf; inf.InList.Add(lf); inf.InList.Add(lf2); return inf; } } } return null; } protected InFragment TryMergeBinaryFragmentAndInFragment(BinaryFragment bf, InFragment inf) { if( !bf.IsNegated && ( bf.Operator == "=" )) { ColumnFragment cf; LiteralFragment lf; GetBinaryFragmentPartsForIn(bf, out lf, out cf); if ((lf != null) && (cf != null)) { if (inf.Argument.Equals(cf)) { if (!inf.InList.Contains(lf)) inf.InList.Add(lf); return inf; } } } return null; } protected void GetBinaryFragmentPartsForIn(BinaryFragment bf, out LiteralFragment lf, out ColumnFragment cf) { cf = bf.Right as ColumnFragment; lf = bf.Left as LiteralFragment; if (lf == null) { lf = bf.Right as LiteralFragment; cf = bf.Left as ColumnFragment; } } protected bool ShouldWrapExpression(DbExpression e) { switch (e.ExpressionKind) { case DbExpressionKind.Property: case DbExpressionKind.ParameterReference: case DbExpressionKind.Constant: return false; } return true; } #endregion } }