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