using System; using System.Collections.Generic; using System.Linq.Expressions; using Vit.Linq; using Vit.Linq.ExpressionNodes.ComponentModel; using Vitorm.Entity; using Vitorm.MySql.TranslateService; using Vitorm.Sql.SqlTranslate; namespace Vitorm.MySql { public class SqlTranslateService : Vitorm.Sql.SqlTranslate.SqlTranslateService { public static readonly SqlTranslateService Instance = new SqlTranslateService(); protected override BaseQueryTranslateService queryTranslateService { get; } protected override BaseQueryTranslateService executeUpdateTranslateService { get; } protected override BaseQueryTranslateService executeDeleteTranslateService { get; } public SqlTranslateService() { queryTranslateService = new QueryTranslateService(this); executeUpdateTranslateService = new ExecuteUpdateTranslateService(this); executeDeleteTranslateService = new ExecuteDeleteTranslateService(this); } /// /// Generates the delimited SQL representation of an identifier (column name, table name, etc.). /// /// The identifier to delimit. /// /// The generated string. /// public override string DelimitIdentifier(string identifier) => $"`{EscapeIdentifier(identifier)}`"; // Interpolation okay; strings /// /// Generates the escaped SQL representation of an identifier (column name, table name, etc.). /// /// The identifier to be escaped. /// /// The generated string. /// public override string EscapeIdentifier(string identifier) => identifier?.Replace("`", "\\`"); #region EvalExpression /// /// read where or value or on /// /// /// /// /// public override string EvalExpression(QueryTranslateArgument arg, ExpressionNode node) { switch (node.nodeType) { case NodeType.MethodCall: { ExpressionNode_MethodCall methodCall = node; switch (methodCall.methodName) { // ##1 ToString case nameof(object.ToString): { return $"cast({EvalExpression(arg, methodCall.@object)} as char)"; } #region ##2 String method: StartsWith EndsWith Contains case nameof(string.StartsWith): // String.StartsWith { var str = methodCall.@object; var value = methodCall.arguments[0]; return $"{EvalExpression(arg, str)} like concat({EvalExpression(arg, value)},'%')"; } case nameof(string.EndsWith): // String.EndsWith { var str = methodCall.@object; var value = methodCall.arguments[0]; return $"{EvalExpression(arg, str)} like concat('%',{EvalExpression(arg, value)})"; } case nameof(string.Contains) when methodCall.methodCall_typeName == "String": // String.Contains { var str = methodCall.@object; var value = methodCall.arguments[0]; return $"{EvalExpression(arg, str)} like concat('%',{EvalExpression(arg, value)},'%')"; } #endregion } break; } #region Read Value case NodeType.Convert: { // cast( 4.1 as signed) ExpressionNode_Convert convert = node; Type targetType = convert.valueType?.ToType(); if (targetType == typeof(object)) return EvalExpression(arg, convert.body); // Nullable if (targetType.IsGenericType) targetType = targetType.GetGenericArguments()[0]; string targetDbType = GetColumnDbType(targetType); var sourceType = convert.body.Member_GetType(); if (sourceType != null) { if (sourceType.IsGenericType) sourceType = sourceType.GetGenericArguments()[0]; if (targetDbType == GetColumnDbType(sourceType)) return EvalExpression(arg, convert.body); } if (targetType == typeof(string)) { return $"cast({EvalExpression(arg, convert.body)} as char)"; } return $"cast({EvalExpression(arg, convert.body)} as {targetDbType})"; } case nameof(ExpressionType.Add): { ExpressionNode_Binary binary = node; // ##1 String Add if (node.valueType?.ToType() == typeof(string)) { // select ifNull( cast( (userFatherId) as char ) , '' ) from `User` return $"CONCAT( {BuildSqlSentence(binary.left)} , {BuildSqlSentence(binary.right)} )"; string BuildSqlSentence(ExpressionNode node) { if (node.nodeType == NodeType.Constant) { ExpressionNode_Constant constant = node; if (constant.value == null) return "''"; else return $"cast( ({EvalExpression(arg, node)}) as char )"; } else return $"ifNull( cast( ({EvalExpression(arg, node)}) as char ) , '')"; } } // ##2 Numeric Add return $"{EvalExpression(arg, binary.left)} + {EvalExpression(arg, binary.right)}"; } case nameof(ExpressionType.Coalesce): { ExpressionNode_Binary binary = node; return $"COALESCE({EvalExpression(arg, binary.left)},{EvalExpression(arg, binary.right)})"; } case nameof(ExpressionType.Conditional): { // IF(500<1000,true,false) ExpressionNode_Conditional conditional = node; return $"IF({EvalExpression(arg, conditional.Conditional_GetTest())},{EvalExpression(arg, conditional.Conditional_GetIfTrue())},{EvalExpression(arg, conditional.Conditional_GetIfFalse())})"; } #endregion } return base.EvalExpression(arg, node); } #endregion #region PrepareCreate public override string PrepareTryCreateTable(IEntityDescriptor entityDescriptor) { /* //sql CREATE TABLE IF NOT EXISTS `User` ( id int PRIMARY KEY AUTO_INCREMENT NOT NULL, name varchar(100) DEFAULT NULL, birth date DEFAULT NULL, fatherId int DEFAULT NULL, motherId int DEFAULT NULL ) ; */ List sqlFields = new(); // #1 primary key if (entityDescriptor.key != null) sqlFields.Add(GetColumnSql(entityDescriptor.key)); // #2 columns entityDescriptor.propertiesWithoutKey?.ForEach(column => sqlFields.Add(GetColumnSql(column))); return $@" CREATE TABLE IF NOT EXISTS {DelimitTableName(entityDescriptor)} ( {string.Join(",\r\n ", sqlFields)} )"; string GetColumnSql(IPropertyDescriptor column) { var isNullable = !column.isKey && column.isNullable; var columnDbType = column.columnDbType ?? GetColumnDbType(column); var defaultValue = isNullable ? "default null" : ""; if (column.isIdentity) { var type = TypeUtil.GetUnderlyingType(column.type); if (type == typeof(Guid)) { } else defaultValue = "AUTO_INCREMENT"; } var nullable = isNullable ? "" : "not null"; // https://mysql.net.cn/doc/refman/8.0/en/create-table.html /* name type primaryKey defaultValue nullable id int primary key default null not null/null AUTO_INCREMENT */ return $" {DelimitIdentifier(column.columnName)} {columnDbType} {(column.isKey ? "primary key" : "")} {defaultValue} {nullable}"; } } public readonly static Dictionary columnDbTypeMap = new() { [typeof(DateTime)] = "DATETIME", [typeof(string)] = "varchar(1000)", [typeof(float)] = "FLOAT", [typeof(double)] = "DOUBLE", [typeof(decimal)] = "DOUBLE", [typeof(Int32)] = "INTEGER", [typeof(Int16)] = "SMALLINT", [typeof(byte)] = "TINYINT", [typeof(bool)] = "TINYINT", [typeof(Guid)] = "binary(16)", }; protected override string GetColumnDbType(IPropertyDescriptor column) { Type type = column.type; if (column.isKey && column.type == typeof(string)) { // avoid issue: SQL Error [1071] [42000]: Specified key was too long; max key length is 3072 bytes. // example "CREATE TABLE test ( `name` varchar(1000) primary key);" return "varchar(200)"; } if (column.columnLength.HasValue && type == typeof(string)) { // varchar(1000) return $"varchar({(column.columnLength.Value)})"; } return GetColumnDbType(type); } protected override string GetColumnDbType(Type type) { var underlyingType = TypeUtil.GetUnderlyingType(type); if (columnDbTypeMap.TryGetValue(underlyingType, out var dbType)) return dbType; if (underlyingType.Name.ToLower().Contains("int")) return "INTEGER"; throw new NotSupportedException("unsupported column type:" + underlyingType.Name); } #endregion public override string PrepareTryDropTable(IEntityDescriptor entityDescriptor) { // drop table if exists `User`; return $@"drop table if exists {DelimitTableName(entityDescriptor)};"; } public override (string sql, Func> GetSqlParams) PrepareAdd(SqlTranslateArgument arg, EAddType addType) { if (addType == EAddType.identityKey) { // insert into user(name,fatherId,motherId) values('',0,0); select last_insert_id(); var entityDescriptor = arg.entityDescriptor; var (columnNames, sqlColumnParams, GetSqlParams) = PrepareAdd_Columns(arg, entityDescriptor.propertiesWithoutKey); string sql = $@"insert into {DelimitTableName(entityDescriptor)}({string.Join(",", columnNames)}) values({string.Join(",", sqlColumnParams)});"; // get generated id sql += "select last_insert_id();"; return (sql, GetSqlParams); } else { // insert into user(name,fatherId,motherId) values('',0,0); var entityDescriptor = arg.entityDescriptor; var (columnNames, sqlColumnParams, GetSqlParams) = PrepareAdd_Columns(arg, entityDescriptor.properties); string sql = $@"insert into {DelimitTableName(entityDescriptor)}({string.Join(",", columnNames)}) values({string.Join(",", sqlColumnParams)});"; return (sql, GetSqlParams); } } } }