diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 212eb91..1109c46 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -7226,6 +7226,9 @@ namespace DynamORM /// Add typed SQL DSL insert assignment. IDynamicTypedInsertQueryBuilder InsertSql(Expression> selector, Func, TypedSqlExpression> valueFactory); + + /// Add typed SQL DSL insert assignments from object projection. + IDynamicTypedInsertQueryBuilder InsertSql(Func, object> values); } /// Typed select query builder for mapped entities. /// Mapped entity type. @@ -7327,6 +7330,9 @@ namespace DynamORM /// Add typed SQL DSL assignment. IDynamicTypedUpdateQueryBuilder SetSql(Expression> selector, Func, TypedSqlExpression> valueFactory); + + /// Add typed SQL DSL assignments from object projection. + IDynamicTypedUpdateQueryBuilder SetSql(Func, object> values); } /// Dynamic update query builder interface. /// This interface it publicly available. Implementation should be hidden. @@ -10724,7 +10730,7 @@ namespace DynamORM } public IDynamicTypedDeleteQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) { - string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName, RenderSubQuery); if (string.IsNullOrEmpty(WhereCondition)) WhereCondition = condition; else @@ -10765,6 +10771,14 @@ namespace DynamORM DynamicSchemaColumn? columnSchema = null; return ParseConstant(value, Parameters, columnSchema); } + private string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query) + { + foreach (KeyValuePair item in query.Parameters) + if (!Parameters.ContainsKey(item.Key)) + Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } /// Typed wrapper over with property-to-column translation. /// Mapped entity type. @@ -10795,12 +10809,30 @@ namespace DynamORM public IDynamicTypedInsertQueryBuilder InsertSql(Expression> selector, Func, TypedSqlExpression> valueFactory) { string column = FixObjectName(TypedModifyHelper.GetMappedColumn(selector), onlyColumn: true); - string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName); + string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName, RenderSubQuery); _columns = _columns == null ? column : string.Format("{0}, {1}", _columns, column); _values = _values == null ? value : string.Format("{0}, {1}", _values, value); return this; } + public IDynamicTypedInsertQueryBuilder InsertSql(Func, object> values) + { + if (values == null) + throw new ArgumentNullException("values"); + + object data = values(new TypedTableContext(null)); + foreach (KeyValuePair item in data.ToDictionary()) + { + string column = FixObjectName(TypedModifyHelper.GetMappedColumnByName(typeof(T), item.Key), onlyColumn: true); + string value = (item.Value as TypedSqlExpression) != null + ? TypedModifyHelper.RenderExpression(u => (TypedSqlExpression)item.Value, null, RenderValue, Database.DecorateName, RenderSubQuery) + : RenderValue(item.Value); + + _columns = _columns == null ? column : string.Format("{0}, {1}", _columns, column); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + } + return this; + } public new IDynamicTypedInsertQueryBuilder Values(Func fn, params Func[] func) { base.Values(fn, func); @@ -10824,6 +10856,14 @@ namespace DynamORM DynamicSchemaColumn? columnSchema = null; return ParseConstant(value, Parameters, columnSchema); } + private string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query) + { + foreach (KeyValuePair item in query.Parameters) + if (!Parameters.ContainsKey(item.Key)) + Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } /// Typed wrapper over with property-to-column translation. /// Mapped entity type. @@ -10863,6 +10903,17 @@ namespace DynamORM { return _builder.Database.DecorateName(name); } + public string RenderSubQuery(IDynamicSelectQueryBuilder query) + { + if (query == null) + throw new ArgumentNullException("query"); + + foreach (var item in query.Parameters) + if (!_builder.Parameters.ContainsKey(item.Key)) + _builder.Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } private readonly DynamicTypeMap _mapper; @@ -11633,7 +11684,7 @@ namespace DynamORM } public IDynamicTypedUpdateQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) { - string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName, RenderSubQuery); if (string.IsNullOrEmpty(WhereCondition)) WhereCondition = condition; else @@ -11644,11 +11695,29 @@ namespace DynamORM public IDynamicTypedUpdateQueryBuilder SetSql(Expression> selector, Func, TypedSqlExpression> valueFactory) { string column = FixObjectName(TypedModifyHelper.GetMappedColumn(selector), onlyColumn: true); - string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName); + string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName, RenderSubQuery); string assignment = string.Format("{0} = {1}", column, value); _columns = _columns == null ? assignment : string.Format("{0}, {1}", _columns, assignment); return this; } + public IDynamicTypedUpdateQueryBuilder SetSql(Func, object> values) + { + if (values == null) + throw new ArgumentNullException("values"); + + object data = values(new TypedTableContext(null)); + foreach (KeyValuePair item in data.ToDictionary()) + { + string column = FixObjectName(TypedModifyHelper.GetMappedColumnByName(typeof(T), item.Key), onlyColumn: true); + string value = (item.Value as TypedSqlExpression) != null + ? TypedModifyHelper.RenderExpression(u => (TypedSqlExpression)item.Value, null, RenderValue, Database.DecorateName, RenderSubQuery) + : RenderValue(item.Value); + + string assignment = string.Format("{0} = {1}", column, value); + _columns = _columns == null ? assignment : string.Format("{0}, {1}", _columns, assignment); + } + return this; + } public new IDynamicTypedUpdateQueryBuilder Update(string column, object value) { base.Update(column, value); @@ -11707,6 +11776,14 @@ namespace DynamORM DynamicSchemaColumn? columnSchema = null; return ParseConstant(value, Parameters, columnSchema); } + private string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query) + { + foreach (KeyValuePair item in query.Parameters) + if (!Parameters.ContainsKey(item.Key)) + Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } /// Update query builder. internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere @@ -11996,12 +12073,14 @@ namespace DynamORM private readonly Func, string> _resolveColumn; private readonly Func _renderValue; private readonly Func _decorateName; + private readonly Func _renderSubQuery; - public ModifyRenderContext(Func, string> resolveColumn, Func renderValue, Func decorateName) + public ModifyRenderContext(Func, string> resolveColumn, Func renderValue, Func decorateName, Func renderSubQuery) { _resolveColumn = resolveColumn; _renderValue = renderValue; _decorateName = decorateName; + _renderSubQuery = renderSubQuery; } public string ResolveColumn(Type modelType, string memberName, string alias) { @@ -12015,6 +12094,10 @@ namespace DynamORM { return _decorateName(name); } + public string RenderSubQuery(IDynamicSelectQueryBuilder query) + { + return _renderSubQuery(query); + } } public static string GetMappedColumn(Expression> selector) { @@ -12036,24 +12119,26 @@ namespace DynamORM Func, TypedSqlPredicate> predicate, string alias, Func renderValue, - Func decorateName) + Func decorateName, + Func renderSubQuery) { if (predicate == null) throw new ArgumentNullException("predicate"); - ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName, renderSubQuery); return predicate(new TypedTableContext(alias)).Render(context); } public static string RenderExpression( Func, TypedSqlExpression> expression, string alias, Func renderValue, - Func decorateName) + Func decorateName, + Func renderSubQuery) { if (expression == null) throw new ArgumentNullException("expression"); - ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName, renderSubQuery); return expression(new TypedTableContext(alias)).Render(context); } private static void ApplyWhereInternal(Type modelType, Action addCondition, Expression expression) @@ -12120,7 +12205,7 @@ namespace DynamORM ? decorateName(mapped) : string.Format("{0}.{1}", alias, decorateName(mapped)); } - private static string GetMappedColumnByName(Type modelType, string memberName) + internal static string GetMappedColumnByName(Type modelType, string memberName) { DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType); if (mapper == null) @@ -15728,6 +15813,9 @@ namespace DynamORM /// Decorate SQL identifier. string DecorateName(string name); + + /// Render subquery SQL and merge any parameters into current context. + string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query); } /// Entry point for the typed SQL DSL. public static class Sql @@ -15767,6 +15855,71 @@ namespace DynamORM { return Func("COALESCE", expressions); } + /// Create SUM expression. + public static TypedSqlExpression Sum(TypedSqlExpression expression) + { + return Func("SUM", expression); + } + /// Create AVG expression. + public static TypedSqlExpression Avg(TypedSqlExpression expression) + { + return Func("AVG", expression); + } + /// Create MIN expression. + public static TypedSqlExpression Min(TypedSqlExpression expression) + { + return Func("MIN", expression); + } + /// Create MAX expression. + public static TypedSqlExpression Max(TypedSqlExpression expression) + { + return Func("MAX", expression); + } + /// Create ABS expression. + public static TypedSqlExpression Abs(TypedSqlExpression expression) + { + return Func("ABS", expression); + } + /// Create UPPER expression. + public static TypedSqlExpression Upper(TypedSqlExpression expression) + { + return Func("UPPER", expression); + } + /// Create LOWER expression. + public static TypedSqlExpression Lower(TypedSqlExpression expression) + { + return Func("LOWER", expression); + } + /// Create TRIM expression. + public static TypedSqlExpression Trim(TypedSqlExpression expression) + { + return Func("TRIM", expression); + } + /// Create LENGTH expression. + public static TypedSqlExpression Length(TypedSqlExpression expression) + { + return Func("LENGTH", expression); + } + /// Create NULLIF expression. + public static TypedSqlExpression NullIf(TypedSqlExpression left, TypedSqlExpression right) + { + return Func("NULLIF", left, right); + } + /// Create CURRENT_TIMESTAMP expression. + public static TypedSqlExpression CurrentTimestamp() + { + return Raw("CURRENT_TIMESTAMP"); + } + /// Create scalar subquery expression. + public static TypedSqlExpression SubQuery(IDynamicSelectQueryBuilder query) + { + return new TypedSqlSubQueryExpression(query); + } + /// Create EXISTS predicate. + public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query) + { + return new TypedSqlExistsPredicate(query); + } /// Create CASE expression builder. public static TypedSqlCaseBuilder Case() { @@ -15861,6 +16014,26 @@ namespace DynamORM { return new TypedSqlUnaryPredicate(this, "IS NOT NULL"); } + /// LIKE predicate. + public TypedSqlPredicate Like(string pattern) + { + return new TypedSqlBinaryPredicate(this, "LIKE", Sql.Val(pattern)); + } + /// IN predicate. + public TypedSqlPredicate In(params object[] values) + { + return new TypedSqlInPredicate(this, values); + } + /// IN predicate. + public TypedSqlPredicate In(IEnumerable values) + { + return new TypedSqlInPredicate(this, values); + } + /// BETWEEN predicate. + public TypedSqlPredicate Between(object lower, object upper) + { + return new TypedSqlBetweenPredicate(this, lower is TypedSqlExpression ? (TypedSqlExpression)lower : Sql.Val(lower), upper is TypedSqlExpression ? (TypedSqlExpression)upper : Sql.Val(upper)); + } } /// Typed SQL expression. public abstract class TypedSqlExpression : TypedSqlExpression @@ -15934,7 +16107,7 @@ namespace DynamORM return context.ResolveColumn(_modelType, _memberName, _alias); } } - internal sealed class TypedSqlValueExpression : TypedSqlExpression + internal sealed class TypedSqlValueExpression : TypedSqlExpression, ITypedSqlNullValue { private readonly object _value; @@ -15946,6 +16119,10 @@ namespace DynamORM { return context.RenderValue(_value); } + public bool IsNullValue + { + get { return _value == null; } + } } internal sealed class TypedSqlRawExpression : TypedSqlExpression { @@ -16009,12 +16186,56 @@ namespace DynamORM internal override string Render(ITypedSqlRenderContext context) { string op = _operator; - if (_right is TypedSqlValueExpression && string.Equals(_right.Render(context), "NULL", StringComparison.OrdinalIgnoreCase)) + TypedSqlValueExpression objRight = _right as TypedSqlValueExpression; + if ((objRight != null && objRight.IsNullValue) || (_right is ITypedSqlNullValue && ((ITypedSqlNullValue)_right).IsNullValue)) op = _operator == "=" ? "IS" : _operator == "<>" ? "IS NOT" : _operator; return string.Format("({0} {1} {2})", _left.Render(context), op, _right.Render(context)); } } + internal interface ITypedSqlNullValue + { + bool IsNullValue { get; } + } + internal sealed class TypedSqlInPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _left; + private readonly IEnumerable _values; + + internal TypedSqlInPredicate(TypedSqlExpression left, IEnumerable values) + { + _left = left; + _values = values; + } + internal override string Render(ITypedSqlRenderContext context) + { + List rendered = new List(); + foreach (object value in _values) + rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context)); + + if (rendered.Count == 0) + return "(1 = 0)"; + + return string.Format("({0} IN({1}))", _left.Render(context), string.Join(", ", rendered.ToArray())); + } + } + internal sealed class TypedSqlBetweenPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _left; + private readonly TypedSqlExpression _lower; + private readonly TypedSqlExpression _upper; + + internal TypedSqlBetweenPredicate(TypedSqlExpression left, TypedSqlExpression lower, TypedSqlExpression upper) + { + _left = left; + _lower = lower; + _upper = upper; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0} BETWEEN {1} AND {2})", _left.Render(context), _lower.Render(context), _upper.Render(context)); + } + } internal sealed class TypedSqlCombinedPredicate : TypedSqlPredicate { private readonly TypedSqlPredicate _left; @@ -16070,6 +16291,32 @@ namespace DynamORM return string.Join(" ", items.ToArray()); } } + internal sealed class TypedSqlSubQueryExpression : TypedSqlExpression + { + private readonly IDynamicSelectQueryBuilder _query; + + internal TypedSqlSubQueryExpression(IDynamicSelectQueryBuilder query) + { + _query = query; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0})", context.RenderSubQuery(_query)); + } + } + internal sealed class TypedSqlExistsPredicate : TypedSqlPredicate + { + private readonly IDynamicSelectQueryBuilder _query; + + internal TypedSqlExistsPredicate(IDynamicSelectQueryBuilder query) + { + _query = query; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("(EXISTS ({0}))", context.RenderSubQuery(_query)); + } + } /// Typed table context used by the typed SQL DSL. /// Mapped entity type. public sealed class TypedTableContext diff --git a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs index 39695b6..7b2e49c 100644 --- a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs +++ b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs @@ -115,5 +115,134 @@ namespace DynamORM.Tests.TypedSql "DELETE FROM \"sample_users\" WHERE ((\"id\" = [$0]) AND (\"code\" <> [$1]))", NormalizeSql(cmd.CommandText())); } + + [Test] + public void TestWhereSqlSupportsLikeInAndBetween() + { + var cmd = Database.FromTyped("u") + .WhereSql(u => u.Col(x => x.Code).Like("A%") + .And(u.Col(x => x.Id).In(1, 2, 3)) + .And(u.Col(x => x.Id).Between(1, 10))) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (((u.\"user_code\" LIKE [$0]) AND (u.\"id_user\" IN([$1], [$2], [$3]))) AND (u.\"id_user\" BETWEEN [$4] AND [$5]))", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestWhereSqlSupportsEmptyIn() + { + var cmd = Database.FromTyped("u") + .WhereSql(u => u.Col(x => x.Id).In(new int[0])) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (1 = 0)", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestSelectSqlSupportsStandardFunctions() + { + var cmd = Database.FromTyped("u") + .SelectSql( + u => Sql.Sum(u.Col(x => x.Id)).As("sum_id"), + u => Sql.Avg(u.Col(x => x.Id)).As("avg_id"), + u => Sql.Min(u.Col(x => x.Id)).As("min_id"), + u => Sql.Max(u.Col(x => x.Id)).As("max_id"), + u => Sql.Abs(Sql.Val(-5)).As("abs_value"), + u => Sql.Upper(u.Col(x => x.Code)).As("upper_code"), + u => Sql.Lower(u.Col(x => x.Code)).As("lower_code"), + u => Sql.Trim(u.Col(x => x.Code)).As("trim_code"), + u => Sql.Length(u.Col(x => x.Code)).As("len_code"), + u => Sql.NullIf(u.Col(x => x.Code), Sql.Val("X")).As("nullif_code"), + u => Sql.CurrentTimestamp().As("ts")); + + Assert.AreEqual( + "SELECT SUM(u.\"id_user\") AS \"sum_id\", AVG(u.\"id_user\") AS \"avg_id\", MIN(u.\"id_user\") AS \"min_id\", MAX(u.\"id_user\") AS \"max_id\", ABS([$0]) AS \"abs_value\", UPPER(u.\"user_code\") AS \"upper_code\", LOWER(u.\"user_code\") AS \"lower_code\", TRIM(u.\"user_code\") AS \"trim_code\", LENGTH(u.\"user_code\") AS \"len_code\", NULLIF(u.\"user_code\", [$1]) AS \"nullif_code\", CURRENT_TIMESTAMP AS \"ts\" FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestSelectSqlSupportsCustomFunction() + { + var cmd = Database.FromTyped("u") + .SelectSql(u => Sql.Func("CUSTOM_FUNC", u.Col(x => x.Code), Sql.Val(5)).As("custom_value")); + + Assert.AreEqual( + "SELECT CUSTOM_FUNC(u.\"user_code\", [$0]) AS \"custom_value\" FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestSelectSqlSupportsScalarSubQuery() + { + var sq = Database.From(x => x.sample_users.As("x")) + .Select(x => x.x.id_user) + .Where(x => x.x.user_code == "A"); + + var cmd = Database.FromTyped("u") + .SelectSql(u => Sql.SubQuery(sq).As("sub_id")); + + Assert.AreEqual( + "SELECT (SELECT x.\"id_user\" FROM \"sample_users\" AS x WHERE (x.\"user_code\" = [$0])) AS \"sub_id\" FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestWhereSqlSupportsExists() + { + var sq = Database.From(x => x.sample_users.As("x")) + .Select(x => x.x.id_user) + .Where(x => x.x.user_code == "A"); + + var cmd = Database.FromTyped("u") + .WhereSql(u => Sql.Exists(sq)) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (EXISTS (SELECT x.\"id_user\" FROM \"sample_users\" AS x WHERE (x.\"user_code\" = [$0])))", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestInsertSqlObjectProjection() + { + var sq = Database.From(x => x.sample_users.As("x")) + .Select(x => x.x.user_code) + .Where(x => x.x.id_user == 1); + + var cmd = Database.InsertTyped() + .InsertSql(u => new + { + Code = Sql.SubQuery(sq), + First = Sql.Upper(Sql.Val("typed")) + }); + + Assert.AreEqual( + "INSERT INTO \"sample_users\" (\"code\", \"first\") VALUES ((SELECT x.\"user_code\" FROM \"sample_users\" AS x WHERE (x.\"id_user\" = [$0])), UPPER([$1]))", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestUpdateSqlObjectProjection() + { + var sq = Database.From(x => x.sample_users.As("x")) + .Select(x => x.x.user_code) + .Where(x => x.x.id_user == 1); + + var cmd = Database.UpdateTyped() + .SetSql(u => new + { + Code = Sql.SubQuery(sq), + First = Sql.Lower(Sql.Val("TYPED")) + }) + .WhereSql(u => u.Col(x => x.Id).Eq(1)); + + Assert.AreEqual( + "UPDATE \"sample_users\" SET \"code\" = (SELECT x.\"user_code\" FROM \"sample_users\" AS x WHERE (x.\"id_user\" = [$0])), \"first\" = LOWER([$1]) WHERE (\"id\" = [$2])", + NormalizeSql(cmd.CommandText())); + } } } diff --git a/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs index 869eddd..87a6f1c 100644 --- a/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs @@ -28,5 +28,8 @@ namespace DynamORM.Builders /// Add typed SQL DSL insert assignment. IDynamicTypedInsertQueryBuilder InsertSql(Expression> selector, Func, TypedSqlExpression> valueFactory); + + /// Add typed SQL DSL insert assignments from object projection. + IDynamicTypedInsertQueryBuilder InsertSql(Func, object> values); } } diff --git a/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs index d9842bc..5d3aa9e 100644 --- a/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs @@ -36,5 +36,8 @@ namespace DynamORM.Builders /// Add typed SQL DSL assignment. IDynamicTypedUpdateQueryBuilder SetSql(Expression> selector, Func, TypedSqlExpression> valueFactory); + + /// Add typed SQL DSL assignments from object projection. + IDynamicTypedUpdateQueryBuilder SetSql(Func, object> values); } } diff --git a/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs index 33403e4..9b94847 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.Linq.Expressions; using DynamORM.Builders.Extensions; using DynamORM.TypedSql; @@ -37,7 +38,7 @@ namespace DynamORM.Builders.Implementation public IDynamicTypedDeleteQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) { - string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName, RenderSubQuery); if (string.IsNullOrEmpty(WhereCondition)) WhereCondition = condition; else @@ -84,5 +85,14 @@ namespace DynamORM.Builders.Implementation DynamicSchemaColumn? columnSchema = null; return ParseConstant(value, Parameters, columnSchema); } + + private string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query) + { + foreach (KeyValuePair item in query.Parameters) + if (!Parameters.ContainsKey(item.Key)) + Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } } diff --git a/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs index 4c087ce..7436587 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs @@ -5,8 +5,10 @@ */ using System; +using System.Collections.Generic; using System.Linq.Expressions; using DynamORM.Builders.Extensions; +using DynamORM.Helpers.Dynamics; using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation @@ -44,13 +46,33 @@ namespace DynamORM.Builders.Implementation public IDynamicTypedInsertQueryBuilder InsertSql(Expression> selector, Func, TypedSqlExpression> valueFactory) { string column = FixObjectName(TypedModifyHelper.GetMappedColumn(selector), onlyColumn: true); - string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName); + string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName, RenderSubQuery); _columns = _columns == null ? column : string.Format("{0}, {1}", _columns, column); _values = _values == null ? value : string.Format("{0}, {1}", _values, value); return this; } + public IDynamicTypedInsertQueryBuilder InsertSql(Func, object> values) + { + if (values == null) + throw new ArgumentNullException("values"); + + object data = values(new TypedTableContext(null)); + foreach (KeyValuePair item in data.ToDictionary()) + { + string column = FixObjectName(TypedModifyHelper.GetMappedColumnByName(typeof(T), item.Key), onlyColumn: true); + string value = (item.Value as TypedSqlExpression) != null + ? TypedModifyHelper.RenderExpression(u => (TypedSqlExpression)item.Value, null, RenderValue, Database.DecorateName, RenderSubQuery) + : RenderValue(item.Value); + + _columns = _columns == null ? column : string.Format("{0}, {1}", _columns, column); + _values = _values == null ? value : string.Format("{0}, {1}", _values, value); + } + + return this; + } + public new IDynamicTypedInsertQueryBuilder Values(Func fn, params Func[] func) { base.Values(fn, func); @@ -77,5 +99,14 @@ namespace DynamORM.Builders.Implementation DynamicSchemaColumn? columnSchema = null; return ParseConstant(value, Parameters, columnSchema); } + + private string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query) + { + foreach (KeyValuePair item in query.Parameters) + if (!Parameters.ContainsKey(item.Key)) + Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } } diff --git a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs index 4f681af..c86b9d4 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs @@ -79,6 +79,18 @@ namespace DynamORM.Builders.Implementation { return _builder.Database.DecorateName(name); } + + public string RenderSubQuery(IDynamicSelectQueryBuilder query) + { + if (query == null) + throw new ArgumentNullException("query"); + + foreach (var item in query.Parameters) + if (!_builder.Parameters.ContainsKey(item.Key)) + _builder.Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } private readonly DynamicTypeMap _mapper; diff --git a/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs index 2fd2752..d636544 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs @@ -5,8 +5,10 @@ */ using System; +using System.Collections.Generic; using System.Linq.Expressions; using DynamORM.Builders.Extensions; +using DynamORM.Helpers.Dynamics; using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation @@ -49,7 +51,7 @@ namespace DynamORM.Builders.Implementation public IDynamicTypedUpdateQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) { - string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName, RenderSubQuery); if (string.IsNullOrEmpty(WhereCondition)) WhereCondition = condition; else @@ -61,12 +63,32 @@ namespace DynamORM.Builders.Implementation public IDynamicTypedUpdateQueryBuilder SetSql(Expression> selector, Func, TypedSqlExpression> valueFactory) { string column = FixObjectName(TypedModifyHelper.GetMappedColumn(selector), onlyColumn: true); - string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName); + string value = TypedModifyHelper.RenderExpression(valueFactory, null, RenderValue, Database.DecorateName, RenderSubQuery); string assignment = string.Format("{0} = {1}", column, value); _columns = _columns == null ? assignment : string.Format("{0}, {1}", _columns, assignment); return this; } + public IDynamicTypedUpdateQueryBuilder SetSql(Func, object> values) + { + if (values == null) + throw new ArgumentNullException("values"); + + object data = values(new TypedTableContext(null)); + foreach (KeyValuePair item in data.ToDictionary()) + { + string column = FixObjectName(TypedModifyHelper.GetMappedColumnByName(typeof(T), item.Key), onlyColumn: true); + string value = (item.Value as TypedSqlExpression) != null + ? TypedModifyHelper.RenderExpression(u => (TypedSqlExpression)item.Value, null, RenderValue, Database.DecorateName, RenderSubQuery) + : RenderValue(item.Value); + + string assignment = string.Format("{0} = {1}", column, value); + _columns = _columns == null ? assignment : string.Format("{0}, {1}", _columns, assignment); + } + + return this; + } + public new IDynamicTypedUpdateQueryBuilder Update(string column, object value) { base.Update(column, value); @@ -135,5 +157,14 @@ namespace DynamORM.Builders.Implementation DynamicSchemaColumn? columnSchema = null; return ParseConstant(value, Parameters, columnSchema); } + + private string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query) + { + foreach (KeyValuePair item in query.Parameters) + if (!Parameters.ContainsKey(item.Key)) + Parameters.Add(item.Key, item.Value); + + return query.CommandText(); + } } } diff --git a/DynamORM/Builders/Implementation/TypedModifyHelper.cs b/DynamORM/Builders/Implementation/TypedModifyHelper.cs index a75309a..421ae80 100644 --- a/DynamORM/Builders/Implementation/TypedModifyHelper.cs +++ b/DynamORM/Builders/Implementation/TypedModifyHelper.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using DynamORM.Builders; using DynamORM.Mapper; using DynamORM.TypedSql; @@ -21,12 +22,14 @@ namespace DynamORM.Builders.Implementation private readonly Func, string> _resolveColumn; private readonly Func _renderValue; private readonly Func _decorateName; + private readonly Func _renderSubQuery; - public ModifyRenderContext(Func, string> resolveColumn, Func renderValue, Func decorateName) + public ModifyRenderContext(Func, string> resolveColumn, Func renderValue, Func decorateName, Func renderSubQuery) { _resolveColumn = resolveColumn; _renderValue = renderValue; _decorateName = decorateName; + _renderSubQuery = renderSubQuery; } public string ResolveColumn(Type modelType, string memberName, string alias) @@ -43,6 +46,11 @@ namespace DynamORM.Builders.Implementation { return _decorateName(name); } + + public string RenderSubQuery(IDynamicSelectQueryBuilder query) + { + return _renderSubQuery(query); + } } public static string GetMappedColumn(Expression> selector) @@ -67,12 +75,13 @@ namespace DynamORM.Builders.Implementation Func, TypedSqlPredicate> predicate, string alias, Func renderValue, - Func decorateName) + Func decorateName, + Func renderSubQuery) { if (predicate == null) throw new ArgumentNullException("predicate"); - ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName, renderSubQuery); return predicate(new TypedTableContext(alias)).Render(context); } @@ -80,12 +89,13 @@ namespace DynamORM.Builders.Implementation Func, TypedSqlExpression> expression, string alias, Func renderValue, - Func decorateName) + Func decorateName, + Func renderSubQuery) { if (expression == null) throw new ArgumentNullException("expression"); - ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName, renderSubQuery); return expression(new TypedTableContext(alias)).Render(context); } @@ -158,7 +168,7 @@ namespace DynamORM.Builders.Implementation : string.Format("{0}.{1}", alias, decorateName(mapped)); } - private static string GetMappedColumnByName(Type modelType, string memberName) + internal static string GetMappedColumnByName(Type modelType, string memberName) { DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType); if (mapper == null) diff --git a/DynamORM/TypedSql/ITypedSqlRenderContext.cs b/DynamORM/TypedSql/ITypedSqlRenderContext.cs index 58ed69d..203b3e0 100644 --- a/DynamORM/TypedSql/ITypedSqlRenderContext.cs +++ b/DynamORM/TypedSql/ITypedSqlRenderContext.cs @@ -19,5 +19,8 @@ namespace DynamORM.TypedSql /// Decorate SQL identifier. string DecorateName(string name); + + /// Render subquery SQL and merge any parameters into current context. + string RenderSubQuery(Builders.IDynamicSelectQueryBuilder query); } } diff --git a/DynamORM/TypedSql/Sql.cs b/DynamORM/TypedSql/Sql.cs index 91f46d5..757eb6b 100644 --- a/DynamORM/TypedSql/Sql.cs +++ b/DynamORM/TypedSql/Sql.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using DynamORM.Builders; namespace DynamORM.TypedSql { @@ -54,6 +55,84 @@ namespace DynamORM.TypedSql return Func("COALESCE", expressions); } + /// Create SUM expression. + public static TypedSqlExpression Sum(TypedSqlExpression expression) + { + return Func("SUM", expression); + } + + /// Create AVG expression. + public static TypedSqlExpression Avg(TypedSqlExpression expression) + { + return Func("AVG", expression); + } + + /// Create MIN expression. + public static TypedSqlExpression Min(TypedSqlExpression expression) + { + return Func("MIN", expression); + } + + /// Create MAX expression. + public static TypedSqlExpression Max(TypedSqlExpression expression) + { + return Func("MAX", expression); + } + + /// Create ABS expression. + public static TypedSqlExpression Abs(TypedSqlExpression expression) + { + return Func("ABS", expression); + } + + /// Create UPPER expression. + public static TypedSqlExpression Upper(TypedSqlExpression expression) + { + return Func("UPPER", expression); + } + + /// Create LOWER expression. + public static TypedSqlExpression Lower(TypedSqlExpression expression) + { + return Func("LOWER", expression); + } + + /// Create TRIM expression. + public static TypedSqlExpression Trim(TypedSqlExpression expression) + { + return Func("TRIM", expression); + } + + /// Create LENGTH expression. + public static TypedSqlExpression Length(TypedSqlExpression expression) + { + return Func("LENGTH", expression); + } + + /// Create NULLIF expression. + public static TypedSqlExpression NullIf(TypedSqlExpression left, TypedSqlExpression right) + { + return Func("NULLIF", left, right); + } + + /// Create CURRENT_TIMESTAMP expression. + public static TypedSqlExpression CurrentTimestamp() + { + return Raw("CURRENT_TIMESTAMP"); + } + + /// Create scalar subquery expression. + public static TypedSqlExpression SubQuery(IDynamicSelectQueryBuilder query) + { + return new TypedSqlSubQueryExpression(query); + } + + /// Create EXISTS predicate. + public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query) + { + return new TypedSqlExistsPredicate(query); + } + /// Create CASE expression builder. public static TypedSqlCaseBuilder Case() { diff --git a/DynamORM/TypedSql/TypedSqlExpression.cs b/DynamORM/TypedSql/TypedSqlExpression.cs index 7676013..1bdf68e 100644 --- a/DynamORM/TypedSql/TypedSqlExpression.cs +++ b/DynamORM/TypedSql/TypedSqlExpression.cs @@ -5,7 +5,9 @@ */ using System; +using System.Collections; using System.Collections.Generic; +using DynamORM.Builders; namespace DynamORM.TypedSql { @@ -83,6 +85,30 @@ namespace DynamORM.TypedSql { return new TypedSqlUnaryPredicate(this, "IS NOT NULL"); } + + /// LIKE predicate. + public TypedSqlPredicate Like(string pattern) + { + return new TypedSqlBinaryPredicate(this, "LIKE", Sql.Val(pattern)); + } + + /// IN predicate. + public TypedSqlPredicate In(params object[] values) + { + return new TypedSqlInPredicate(this, values); + } + + /// IN predicate. + public TypedSqlPredicate In(IEnumerable values) + { + return new TypedSqlInPredicate(this, values); + } + + /// BETWEEN predicate. + public TypedSqlPredicate Between(object lower, object upper) + { + return new TypedSqlBetweenPredicate(this, lower is TypedSqlExpression ? (TypedSqlExpression)lower : Sql.Val(lower), upper is TypedSqlExpression ? (TypedSqlExpression)upper : Sql.Val(upper)); + } } /// Typed SQL expression. @@ -167,7 +193,7 @@ namespace DynamORM.TypedSql } } - internal sealed class TypedSqlValueExpression : TypedSqlExpression + internal sealed class TypedSqlValueExpression : TypedSqlExpression, ITypedSqlNullValue { private readonly object _value; @@ -180,6 +206,11 @@ namespace DynamORM.TypedSql { return context.RenderValue(_value); } + + public bool IsNullValue + { + get { return _value == null; } + } } internal sealed class TypedSqlRawExpression : TypedSqlExpression @@ -251,13 +282,62 @@ namespace DynamORM.TypedSql internal override string Render(ITypedSqlRenderContext context) { string op = _operator; - if (_right is TypedSqlValueExpression && string.Equals(_right.Render(context), "NULL", StringComparison.OrdinalIgnoreCase)) + TypedSqlValueExpression objRight = _right as TypedSqlValueExpression; + if ((objRight != null && objRight.IsNullValue) || (_right is ITypedSqlNullValue && ((ITypedSqlNullValue)_right).IsNullValue)) op = _operator == "=" ? "IS" : _operator == "<>" ? "IS NOT" : _operator; return string.Format("({0} {1} {2})", _left.Render(context), op, _right.Render(context)); } } + internal interface ITypedSqlNullValue + { + bool IsNullValue { get; } + } + + internal sealed class TypedSqlInPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _left; + private readonly IEnumerable _values; + + internal TypedSqlInPredicate(TypedSqlExpression left, IEnumerable values) + { + _left = left; + _values = values; + } + + internal override string Render(ITypedSqlRenderContext context) + { + List rendered = new List(); + foreach (object value in _values) + rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context)); + + if (rendered.Count == 0) + return "(1 = 0)"; + + return string.Format("({0} IN({1}))", _left.Render(context), string.Join(", ", rendered.ToArray())); + } + } + + internal sealed class TypedSqlBetweenPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _left; + private readonly TypedSqlExpression _lower; + private readonly TypedSqlExpression _upper; + + internal TypedSqlBetweenPredicate(TypedSqlExpression left, TypedSqlExpression lower, TypedSqlExpression upper) + { + _left = left; + _lower = lower; + _upper = upper; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0} BETWEEN {1} AND {2})", _left.Render(context), _lower.Render(context), _upper.Render(context)); + } + } + internal sealed class TypedSqlCombinedPredicate : TypedSqlPredicate { private readonly TypedSqlPredicate _left; @@ -318,4 +398,34 @@ namespace DynamORM.TypedSql return string.Join(" ", items.ToArray()); } } + + internal sealed class TypedSqlSubQueryExpression : TypedSqlExpression + { + private readonly IDynamicSelectQueryBuilder _query; + + internal TypedSqlSubQueryExpression(IDynamicSelectQueryBuilder query) + { + _query = query; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0})", context.RenderSubQuery(_query)); + } + } + + internal sealed class TypedSqlExistsPredicate : TypedSqlPredicate + { + private readonly IDynamicSelectQueryBuilder _query; + + internal TypedSqlExistsPredicate(IDynamicSelectQueryBuilder query) + { + _query = query; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("(EXISTS ({0}))", context.RenderSubQuery(_query)); + } + } }