From 8b1737a53bc3e982b446ec6776273c43dd0d271d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 08:22:23 +0100 Subject: [PATCH] Add experimental typed SQL DSL for typed builders --- AmalgamationTool/DynamORM.Amalgamation.cs | 705 +++++++++++++++++- DynamORM.Tests/TypedSql/TypedSqlDslTests.cs | 119 +++ .../IDynamicTypedDeleteQueryBuilder.cs | 4 + .../IDynamicTypedInsertQueryBuilder.cs | 4 + .../IDynamicTypedSelectQueryBuilder.cs | 16 + .../IDynamicTypedUpdateQueryBuilder.cs | 7 + .../DynamicInsertQueryBuilder.cs | 6 +- .../DynamicSelectQueryBuilder.cs | 8 +- .../DynamicTypedDeleteQueryBuilder.cs | 21 + .../DynamicTypedInsertQueryBuilder.cs | 20 + .../DynamicTypedSelectQueryBuilder.cs | 153 +++- .../DynamicTypedUpdateQueryBuilder.cs | 30 + .../DynamicUpdateQueryBuilder.cs | 4 +- .../Implementation/TypedModifyHelper.cs | 76 ++ DynamORM/Builders/TypedJoinBuilder.cs | 18 + DynamORM/TypedSql/ITypedSqlRenderContext.cs | 23 + DynamORM/TypedSql/Sql.cs | 91 +++ DynamORM/TypedSql/TypedSqlExpression.cs | 321 ++++++++ DynamORM/TypedSql/TypedTableContext.cs | 40 + 19 files changed, 1649 insertions(+), 17 deletions(-) create mode 100644 DynamORM.Tests/TypedSql/TypedSqlDslTests.cs create mode 100644 DynamORM/TypedSql/ITypedSqlRenderContext.cs create mode 100644 DynamORM/TypedSql/Sql.cs create mode 100644 DynamORM/TypedSql/TypedSqlExpression.cs create mode 100644 DynamORM/TypedSql/TypedTableContext.cs diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 39740b5..212eb91 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -10,6 +10,7 @@ using DynamORM.Builders; using DynamORM.Helpers.Dynamics; using DynamORM.Helpers; using DynamORM.Mapper; +using DynamORM.TypedSql; using DynamORM.Validation; using System.Collections.Concurrent; using System.Collections.Generic; @@ -7203,6 +7204,9 @@ namespace DynamORM /// Predicate to parse. /// Builder instance. IDynamicTypedDeleteQueryBuilder Where(Expression> predicate); + + /// Add typed SQL DSL where predicate. + IDynamicTypedDeleteQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate); } /// Typed insert query builder for mapped entities. /// Mapped entity type. @@ -7219,6 +7223,9 @@ namespace DynamORM /// Mapped object value. /// Builder instance. IDynamicTypedInsertQueryBuilder Insert(T value); + + /// Add typed SQL DSL insert assignment. + IDynamicTypedInsertQueryBuilder InsertSql(Expression> selector, Func, TypedSqlExpression> valueFactory); } /// Typed select query builder for mapped entities. /// Mapped entity type. @@ -7278,6 +7285,21 @@ namespace DynamORM /// Additional selectors to parse. /// Builder instance. IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors); + + /// Add typed SQL DSL select items. + IDynamicTypedSelectQueryBuilder SelectSql(Func, TypedSqlSelectable> selector, params Func, TypedSqlSelectable>[] selectors); + + /// Add typed SQL DSL where predicate. + IDynamicTypedSelectQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate); + + /// Add typed SQL DSL having predicate. + IDynamicTypedSelectQueryBuilder HavingSql(Func, TypedSqlPredicate> predicate); + + /// Add typed SQL DSL group by expressions. + IDynamicTypedSelectQueryBuilder GroupBySql(Func, TypedSqlExpression> selector, params Func, TypedSqlExpression>[] selectors); + + /// Add typed SQL DSL order by expressions. + IDynamicTypedSelectQueryBuilder OrderBySql(Func, TypedSqlOrderExpression> selector, params Func, TypedSqlOrderExpression>[] selectors); } /// Typed update query builder for mapped entities. /// Mapped entity type. @@ -7299,6 +7321,12 @@ namespace DynamORM /// Mapped object value. /// Builder instance. IDynamicTypedUpdateQueryBuilder Values(T value); + + /// Add typed SQL DSL where predicate. + IDynamicTypedUpdateQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate); + + /// Add typed SQL DSL assignment. + IDynamicTypedUpdateQueryBuilder SetSql(Expression> selector, Func, TypedSqlExpression> valueFactory); } /// Dynamic update query builder interface. /// This interface it publicly available. Implementation should be hidden. @@ -7475,6 +7503,9 @@ namespace DynamORM /// Gets raw ON condition. public string OnRawCondition { get; private set; } + /// Gets typed SQL DSL ON specification. + public Func, TypedTableContext, TypedSqlPredicate> OnSqlPredicate { get; private set; } + /// Sets join alias. public TypedJoinBuilder As(string alias) { @@ -7560,6 +7591,7 @@ namespace DynamORM OnPredicate = predicate; OnRawCondition = null; + OnSqlPredicate = null; return this; } /// Sets raw ON clause (without the ON keyword). @@ -7570,6 +7602,18 @@ namespace DynamORM OnRawCondition = condition.Trim(); OnPredicate = null; + OnSqlPredicate = null; + return this; + } + /// Sets typed SQL DSL ON predicate. + public TypedJoinBuilder OnSql(Func, TypedTableContext, TypedSqlPredicate> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + OnSqlPredicate = predicate; + OnPredicate = null; + OnRawCondition = null; return this; } } @@ -8229,8 +8273,8 @@ namespace DynamORM /// Implementation of dynamic insert query builder. internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder { - private string _columns; - private string _values; + protected string _columns; + protected string _values; /// /// Initializes a new instance of the class. @@ -9338,11 +9382,11 @@ namespace DynamORM private int? _offset = null; private bool _distinct = false; - private string _select; + protected string _select; private string _from; protected string _join; - private string _groupby; - private string _orderby; + protected string _groupby; + protected string _orderby; #region IQueryWithHaving @@ -10678,6 +10722,16 @@ namespace DynamORM TypedModifyHelper.ApplyWhere((c, o, v) => base.Where(c, o, v), predicate); return this; } + public IDynamicTypedDeleteQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) + { + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } public new IDynamicTypedDeleteQueryBuilder Where(Func func) { base.Where(func); @@ -10703,6 +10757,14 @@ namespace DynamORM base.Where(conditions, schema); return this; } + private string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return ParseConstant(value, Parameters, columnSchema); + } } /// Typed wrapper over with property-to-column translation. /// Mapped entity type. @@ -10730,6 +10792,15 @@ namespace DynamORM base.Insert(value); return this; } + 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); + + _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); @@ -10745,11 +10816,54 @@ namespace DynamORM base.Insert(o); return this; } + private string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return ParseConstant(value, Parameters, columnSchema); + } } /// Typed wrapper over with property-to-column translation. /// Mapped entity type. internal class DynamicTypedSelectQueryBuilder : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder { + private sealed class TypedSqlRenderContext : ITypedSqlRenderContext + { + private readonly DynamicTypedSelectQueryBuilder _builder; + + public TypedSqlRenderContext(DynamicTypedSelectQueryBuilder builder) + { + _builder = builder; + } + public string ResolveColumn(Type modelType, string memberName, string alias) + { + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType); + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", modelType.FullName)); + + string mappedColumn = mapper.PropertyMap.TryGetValue(memberName) + ?? mapper.PropertyMap.Where(x => string.Equals(x.Key, memberName, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).FirstOrDefault() + ?? memberName; + + return string.IsNullOrEmpty(alias) + ? _builder.Database.DecorateName(mappedColumn) + : string.Format("{0}.{1}", alias, _builder.Database.DecorateName(mappedColumn)); + } + public string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return _builder.ParseConstant(value, _builder.Parameters, columnSchema); + } + public string DecorateName(string name) + { + return _builder.Database.DecorateName(name); + } + } private readonly DynamicTypeMap _mapper; public DynamicTypedSelectQueryBuilder(DynamicDatabase db) @@ -10846,7 +10960,12 @@ namespace DynamORM if (SupportNoLock && spec.UseNoLock) joinExpr += " WITH(NOLOCK)"; - if (!string.IsNullOrEmpty(spec.OnRawCondition)) + if (spec.OnSqlPredicate != null) + { + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + joinExpr += string.Format(" ON {0}", spec.OnSqlPredicate(new TypedTableContext(GetRootAliasOrTableName()), new TypedTableContext(rightAlias)).Render(context)); + } + else if (!string.IsNullOrEmpty(spec.OnRawCondition)) joinExpr += string.Format(" ON {0}", spec.OnRawCondition); AppendJoinClause(joinExpr); @@ -10894,6 +11013,18 @@ namespace DynamORM } return this; } + public IDynamicTypedSelectQueryBuilder SelectSql(Func, TypedSqlSelectable> selector, params Func, TypedSqlSelectable>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddSelectSqlSelector(selector); + if (selectors != null) + foreach (Func, TypedSqlSelectable> item in selectors) + AddSelectSqlSelector(item); + + return this; + } public IDynamicTypedSelectQueryBuilder GroupBy(Expression> selector, params Expression>[] selectors) { if (selector == null) @@ -10911,6 +11042,18 @@ namespace DynamORM } return this; } + public IDynamicTypedSelectQueryBuilder GroupBySql(Func, TypedSqlExpression> selector, params Func, TypedSqlExpression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddGroupBySqlSelector(selector); + if (selectors != null) + foreach (Func, TypedSqlExpression> item in selectors) + AddGroupBySqlSelector(item); + + return this; + } public IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors) { if (selector == null) @@ -10928,6 +11071,18 @@ namespace DynamORM } return this; } + public IDynamicTypedSelectQueryBuilder OrderBySql(Func, TypedSqlOrderExpression> selector, params Func, TypedSqlOrderExpression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddOrderBySqlSelector(selector); + if (selectors != null) + foreach (Func, TypedSqlOrderExpression> item in selectors) + AddOrderBySqlSelector(item); + + return this; + } public new IDynamicTypedSelectQueryBuilder Top(int? top) { base.Top(top); @@ -10962,6 +11117,32 @@ namespace DynamORM return this; } + public IDynamicTypedSelectQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = RenderSqlPredicate(predicate); + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + public IDynamicTypedSelectQueryBuilder HavingSql(Func, TypedSqlPredicate> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = RenderSqlPredicate(predicate); + if (string.IsNullOrEmpty(HavingCondition)) + HavingCondition = condition; + else + HavingCondition = string.Format("{0} AND {1}", HavingCondition, condition); + + return this; + } public new IDynamicTypedSelectQueryBuilder Having(DynamicColumn column) { base.Having(column); @@ -11000,6 +11181,16 @@ namespace DynamORM ((IDynamicSelectQueryBuilder)this).Select(x => parsed); } } + private void AddSelectSqlSelector(Func, TypedSqlSelectable> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + TypedSqlSelectable item = selector(new TypedTableContext(GetRootAliasOrTableName())); + string rendered = item.Render(context); + _select = string.IsNullOrEmpty(_select) ? rendered : string.Format("{0}, {1}", _select, rendered); + } private void AddGroupBySelector(Expression> selector) { var body = UnwrapConvert(selector.Body); @@ -11018,6 +11209,16 @@ namespace DynamORM ((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed); } } + private void AddGroupBySqlSelector(Func, TypedSqlExpression> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + TypedSqlExpression item = selector(new TypedTableContext(GetRootAliasOrTableName())); + string rendered = item.Render(context); + _groupby = string.IsNullOrEmpty(_groupby) ? rendered : string.Format("{0}, {1}", _groupby, rendered); + } private void AddOrderBySelector(Expression> selector) { var body = UnwrapConvert(selector.Body); @@ -11032,6 +11233,21 @@ namespace DynamORM string parsed = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); ((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed); } + private void AddOrderBySqlSelector(Func, TypedSqlOrderExpression> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + TypedSqlOrderExpression item = selector(new TypedTableContext(GetRootAliasOrTableName())); + string rendered = item.Render(context); + _orderby = string.IsNullOrEmpty(_orderby) ? rendered : string.Format("{0}, {1}", _orderby, rendered); + } + private string RenderSqlPredicate(Func, TypedSqlPredicate> predicate) + { + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + return predicate(new TypedTableContext(GetRootAliasOrTableName())).Render(context); + } private string ParseTypedCondition(Expression expression) { expression = UnwrapConvert(expression); @@ -11415,6 +11631,24 @@ namespace DynamORM base.Values(value); return this; } + public IDynamicTypedUpdateQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) + { + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + 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 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); @@ -11465,11 +11699,19 @@ namespace DynamORM base.Where(conditions, schema); return this; } + private string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return ParseConstant(value, Parameters, columnSchema); + } } /// Update query builder. internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere { - private string _columns; + protected string _columns; internal DynamicUpdateQueryBuilder(DynamicDatabase db) : base(db) @@ -11749,6 +11991,31 @@ namespace DynamORM /// Helper methods for typed modify builders. internal static class TypedModifyHelper { + private sealed class ModifyRenderContext : ITypedSqlRenderContext + { + private readonly Func, string> _resolveColumn; + private readonly Func _renderValue; + private readonly Func _decorateName; + + public ModifyRenderContext(Func, string> resolveColumn, Func renderValue, Func decorateName) + { + _resolveColumn = resolveColumn; + _renderValue = renderValue; + _decorateName = decorateName; + } + public string ResolveColumn(Type modelType, string memberName, string alias) + { + return _resolveColumn(modelType, memberName, alias, _decorateName); + } + public string RenderValue(object value) + { + return _renderValue(value); + } + public string DecorateName(string name) + { + return _decorateName(name); + } + } public static string GetMappedColumn(Expression> selector) { if (selector == null) @@ -11765,6 +12032,30 @@ namespace DynamORM ApplyWhereInternal(typeof(T), addCondition, predicate.Body); } + public static string RenderPredicate( + Func, TypedSqlPredicate> predicate, + string alias, + Func renderValue, + Func decorateName) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + return predicate(new TypedTableContext(alias)).Render(context); + } + public static string RenderExpression( + Func, TypedSqlExpression> expression, + string alias, + Func renderValue, + Func decorateName) + { + if (expression == null) + throw new ArgumentNullException("expression"); + + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + return expression(new TypedTableContext(alias)).Render(context); + } private static void ApplyWhereInternal(Type modelType, Action addCondition, Expression expression) { expression = UnwrapConvert(expression); @@ -11822,6 +12113,23 @@ namespace DynamORM .FirstOrDefault() ?? member.Member.Name; } + private static string ResolveColumn(Type modelType, string memberName, string alias, Func decorateName) + { + string mapped = GetMappedColumnByName(modelType, memberName); + return string.IsNullOrEmpty(alias) + ? decorateName(mapped) + : string.Format("{0}.{1}", alias, decorateName(mapped)); + } + private static string GetMappedColumnByName(Type modelType, string memberName) + { + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType); + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", modelType.FullName)); + + return mapper.PropertyMap.TryGetValue(memberName) + ?? mapper.PropertyMap.Where(x => string.Equals(x.Key, memberName, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).FirstOrDefault() + ?? memberName; + } private static Expression UnwrapConvert(Expression expression) { while (expression is UnaryExpression && @@ -15407,6 +15715,389 @@ namespace DynamORM } } } + namespace TypedSql + { + /// Render context used by typed SQL DSL nodes. + public interface ITypedSqlRenderContext + { + /// Resolve mapped column for given model member. + string ResolveColumn(Type modelType, string memberName, string alias); + + /// Render value as SQL parameter or literal fragment. + string RenderValue(object value); + + /// Decorate SQL identifier. + string DecorateName(string name); + } + /// Entry point for the typed SQL DSL. + public static class Sql + { + /// Create parameterized value expression. + public static TypedSqlExpression Val(T value) + { + return new TypedSqlValueExpression(value); + } + /// Create parameterized value expression. + public static TypedSqlExpression Val(object value) + { + return new TypedSqlValueExpression(value); + } + /// Create raw SQL expression. + public static TypedSqlExpression Raw(string sql) + { + return new TypedSqlRawExpression(sql); + } + /// Create generic function call. + public static TypedSqlExpression Func(string name, params TypedSqlExpression[] arguments) + { + return new TypedSqlFunctionExpression(name, arguments); + } + /// Create COUNT(*) expression. + public static TypedSqlExpression Count() + { + return Raw("COUNT(*)"); + } + /// Create COUNT(expr) expression. + public static TypedSqlExpression Count(TypedSqlExpression expression) + { + return Func("COUNT", expression); + } + /// Create COALESCE expression. + public static TypedSqlExpression Coalesce(params TypedSqlExpression[] expressions) + { + return Func("COALESCE", expressions); + } + /// Create CASE expression builder. + public static TypedSqlCaseBuilder Case() + { + return new TypedSqlCaseBuilder(); + } + } + /// Builder for CASE expressions. + /// Result type. + public sealed class TypedSqlCaseBuilder + { + private readonly IList> _cases = new List>(); + + /// Add WHEN ... THEN ... clause. + public TypedSqlCaseBuilder When(TypedSqlPredicate predicate, object value) + { + _cases.Add(new KeyValuePair( + predicate, + value as TypedSqlExpression ?? Sql.Val(value))); + return this; + } + /// Finalize CASE expression with ELSE clause. + public TypedSqlExpression Else(object value) + { + return new TypedSqlCaseExpression(_cases, value as TypedSqlExpression ?? Sql.Val(value)); + } + /// Finalize CASE expression without ELSE clause. + public TypedSqlExpression End() + { + return new TypedSqlCaseExpression(_cases, null); + } + } + /// Base selectable SQL fragment for the typed DSL. + public abstract class TypedSqlSelectable + { + internal abstract string Render(ITypedSqlRenderContext context); + } + /// Base SQL expression for the typed DSL. + public abstract class TypedSqlExpression : TypedSqlSelectable + { + /// Alias this expression in SELECT clause. + public TypedSqlAliasedExpression As(string alias) + { + return new TypedSqlAliasedExpression(this, alias); + } + /// Order ascending. + public TypedSqlOrderExpression Asc() + { + return new TypedSqlOrderExpression(this, true); + } + /// Order descending. + public TypedSqlOrderExpression Desc() + { + return new TypedSqlOrderExpression(this, false); + } + /// Equality predicate. + public TypedSqlPredicate Eq(object value) + { + return new TypedSqlBinaryPredicate(this, "=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + /// Inequality predicate. + public TypedSqlPredicate NotEq(object value) + { + return new TypedSqlBinaryPredicate(this, "<>", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + /// Greater-than predicate. + public TypedSqlPredicate Gt(object value) + { + return new TypedSqlBinaryPredicate(this, ">", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + /// Greater-than-or-equal predicate. + public TypedSqlPredicate Gte(object value) + { + return new TypedSqlBinaryPredicate(this, ">=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + /// Less-than predicate. + public TypedSqlPredicate Lt(object value) + { + return new TypedSqlBinaryPredicate(this, "<", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + /// Less-than-or-equal predicate. + public TypedSqlPredicate Lte(object value) + { + return new TypedSqlBinaryPredicate(this, "<=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + /// IS NULL predicate. + public TypedSqlPredicate IsNull() + { + return new TypedSqlUnaryPredicate(this, "IS NULL"); + } + /// IS NOT NULL predicate. + public TypedSqlPredicate IsNotNull() + { + return new TypedSqlUnaryPredicate(this, "IS NOT NULL"); + } + } + /// Typed SQL expression. + public abstract class TypedSqlExpression : TypedSqlExpression + { + } + /// Typed SQL predicate expression. + public abstract class TypedSqlPredicate : TypedSqlExpression + { + /// Combine with AND. + public TypedSqlPredicate And(TypedSqlPredicate right) + { + return new TypedSqlCombinedPredicate(this, "AND", right); + } + /// Combine with OR. + public TypedSqlPredicate Or(TypedSqlPredicate right) + { + return new TypedSqlCombinedPredicate(this, "OR", right); + } + /// Negate predicate. + public TypedSqlPredicate Not() + { + return new TypedSqlNegatedPredicate(this); + } + } + /// Aliased SQL expression. + public sealed class TypedSqlAliasedExpression : TypedSqlSelectable + { + private readonly TypedSqlExpression _expression; + private readonly string _alias; + + internal TypedSqlAliasedExpression(TypedSqlExpression expression, string alias) + { + _expression = expression; + _alias = alias; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("{0} AS {1}", _expression.Render(context), context.DecorateName(_alias)); + } + } + /// Ordered SQL expression. + public sealed class TypedSqlOrderExpression : TypedSqlSelectable + { + private readonly TypedSqlExpression _expression; + private readonly bool _ascending; + + internal TypedSqlOrderExpression(TypedSqlExpression expression, bool ascending) + { + _expression = expression; + _ascending = ascending; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("{0} {1}", _expression.Render(context), _ascending ? "ASC" : "DESC"); + } + } + internal sealed class TypedSqlColumnExpression : TypedSqlExpression + { + private readonly Type _modelType; + private readonly string _memberName; + private readonly string _alias; + + internal TypedSqlColumnExpression(Type modelType, string memberName, string alias) + { + _modelType = modelType; + _memberName = memberName; + _alias = alias; + } + internal override string Render(ITypedSqlRenderContext context) + { + return context.ResolveColumn(_modelType, _memberName, _alias); + } + } + internal sealed class TypedSqlValueExpression : TypedSqlExpression + { + private readonly object _value; + + internal TypedSqlValueExpression(object value) + { + _value = value; + } + internal override string Render(ITypedSqlRenderContext context) + { + return context.RenderValue(_value); + } + } + internal sealed class TypedSqlRawExpression : TypedSqlExpression + { + private readonly string _sql; + + internal TypedSqlRawExpression(string sql) + { + _sql = sql; + } + internal override string Render(ITypedSqlRenderContext context) + { + return _sql; + } + } + internal sealed class TypedSqlFunctionExpression : TypedSqlExpression + { + private readonly string _name; + private readonly IList _arguments; + + internal TypedSqlFunctionExpression(string name, params TypedSqlExpression[] arguments) + { + _name = name; + _arguments = arguments ?? new TypedSqlExpression[0]; + } + internal override string Render(ITypedSqlRenderContext context) + { + List rendered = new List(); + foreach (TypedSqlExpression argument in _arguments) + rendered.Add(argument.Render(context)); + + return string.Format("{0}({1})", _name, string.Join(", ", rendered.ToArray())); + } + } + internal sealed class TypedSqlUnaryPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _expression; + private readonly string _operator; + + internal TypedSqlUnaryPredicate(TypedSqlExpression expression, string op) + { + _expression = expression; + _operator = op; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0} {1})", _expression.Render(context), _operator); + } + } + internal sealed class TypedSqlBinaryPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _left; + private readonly string _operator; + private readonly TypedSqlExpression _right; + + internal TypedSqlBinaryPredicate(TypedSqlExpression left, string op, TypedSqlExpression right) + { + _left = left; + _operator = op; + _right = right; + } + internal override string Render(ITypedSqlRenderContext context) + { + string op = _operator; + if (_right is TypedSqlValueExpression && string.Equals(_right.Render(context), "NULL", StringComparison.OrdinalIgnoreCase)) + op = _operator == "=" ? "IS" : _operator == "<>" ? "IS NOT" : _operator; + + return string.Format("({0} {1} {2})", _left.Render(context), op, _right.Render(context)); + } + } + internal sealed class TypedSqlCombinedPredicate : TypedSqlPredicate + { + private readonly TypedSqlPredicate _left; + private readonly string _operator; + private readonly TypedSqlPredicate _right; + + internal TypedSqlCombinedPredicate(TypedSqlPredicate left, string op, TypedSqlPredicate right) + { + _left = left; + _operator = op; + _right = right; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0} {1} {2})", _left.Render(context), _operator, _right.Render(context)); + } + } + internal sealed class TypedSqlNegatedPredicate : TypedSqlPredicate + { + private readonly TypedSqlPredicate _predicate; + + internal TypedSqlNegatedPredicate(TypedSqlPredicate predicate) + { + _predicate = predicate; + } + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("(NOT {0})", _predicate.Render(context)); + } + } + internal sealed class TypedSqlCaseExpression : TypedSqlExpression + { + private readonly IList> _cases; + private readonly TypedSqlExpression _elseExpression; + + internal TypedSqlCaseExpression(IList> cases, TypedSqlExpression elseExpression) + { + _cases = cases; + _elseExpression = elseExpression; + } + internal override string Render(ITypedSqlRenderContext context) + { + List items = new List(); + items.Add("CASE"); + + foreach (KeyValuePair item in _cases) + items.Add(string.Format("WHEN {0} THEN {1}", item.Key.Render(context), item.Value.Render(context))); + + if (_elseExpression != null) + items.Add(string.Format("ELSE {0}", _elseExpression.Render(context))); + + items.Add("END"); + return string.Join(" ", items.ToArray()); + } + } + /// Typed table context used by the typed SQL DSL. + /// Mapped entity type. + public sealed class TypedTableContext + { + internal TypedTableContext(string alias) + { + Alias = alias; + } + /// Gets table alias used by the current query. + public string Alias { get; private set; } + + /// Creates a mapped column expression. + public TypedSqlExpression Col(Expression> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + MemberExpression member = selector.Body as MemberExpression; + if (member == null && selector.Body is UnaryExpression) + member = ((UnaryExpression)selector.Body).Operand as MemberExpression; + + if (member == null) + throw new NotSupportedException(string.Format("Column selector must target a mapped property: {0}", selector)); + + return new TypedSqlColumnExpression(typeof(T), member.Member.Name, Alias); + } + } + } namespace Validation { /// Required attribute can be used to validate fields in objects using mapper class. diff --git a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs new file mode 100644 index 0000000..39695b6 --- /dev/null +++ b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs @@ -0,0 +1,119 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System.Linq; +using System.Text.RegularExpressions; +using DynamORM.Tests.Helpers; +using DynamORM.TypedSql; +using NUnit.Framework; + +namespace DynamORM.Tests.TypedSql +{ + [TestFixture] + public class TypedSqlDslTests : TestsBase + { + private static string NormalizeSql(string sql) + { + int index = 0; + return Regex.Replace(sql, @"\[\$[^\]]+\]", m => string.Format("[${0}]", index++)); + } + + [SetUp] + public void SetUp() + { + CreateTestDatabase(); + CreateDynamicDatabase( + DynamicDatabaseOptions.SingleConnection | + DynamicDatabaseOptions.SingleTransaction | + DynamicDatabaseOptions.SupportLimitOffset | + DynamicDatabaseOptions.SupportNoLock); + } + + [TearDown] + public void TearDown() + { + DestroyDynamicDatabase(); + DestroyTestDatabase(); + } + + [Test] + public void TestSelectSqlWithFunctionsAndOrdering() + { + var cmd = Database.FromTyped("u") + .SelectSql( + u => Sql.Count().As("cnt"), + u => Sql.Coalesce(u.Col(x => x.Code), Sql.Val("N/A")).As("code_value")) + .GroupBySql(u => Sql.Coalesce(u.Col(x => x.Code), Sql.Val("N/A"))) + .HavingSql(u => Sql.Count().Gt(1)) + .OrderBySql(u => Sql.Count().Desc()); + + Assert.AreEqual( + "SELECT COUNT(*) AS \"cnt\", COALESCE(u.\"user_code\", [$0]) AS \"code_value\" FROM \"sample_users\" AS u GROUP BY COALESCE(u.\"user_code\", [$1]) HAVING (COUNT(*) > [$2]) ORDER BY COUNT(*) DESC", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestSelectSqlWithCase() + { + var cmd = Database.FromTyped("u") + .SelectSql(u => Sql.Case() + .When(u.Col(x => x.Code).Eq("A"), "Alpha") + .When(u.Col(x => x.Code).Eq("B"), "Beta") + .Else("Other") + .As("category")); + + Assert.AreEqual( + "SELECT CASE WHEN (u.\"user_code\" = [$0]) THEN [$1] WHEN (u.\"user_code\" = [$2]) THEN [$3] ELSE [$4] END AS \"category\" FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestJoinOnSql() + { + var cmd = Database.FromTyped("u") + .Join(j => j.Left().As("x").OnSql((l, r) => l.Col(a => a.Id).Eq(r.Col(a => a.Id)).And(r.Col(a => a.Code).IsNotNull()))) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual("SELECT u.\"id_user\" FROM \"sample_users\" AS u LEFT JOIN \"sample_users\" AS x ON ((u.\"id_user\" = x.\"id_user\") AND (x.\"user_code\" IS NOT NULL))", + cmd.CommandText()); + } + + [Test] + public void TestUpdateSetSqlAndWhereSql() + { + var cmd = Database.UpdateTyped() + .SetSql(u => u.Code, u => Sql.Coalesce(Sql.Val("900"), u.Col(x => x.Code))) + .WhereSql(u => u.Col(x => x.Id).Eq(1).And(u.Col(x => x.Code).IsNotNull())); + + Assert.AreEqual( + "UPDATE \"sample_users\" SET \"code\" = COALESCE([$0], \"code\") WHERE ((\"id\" = [$1]) AND (\"code\" IS NOT NULL))", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestInsertSqlSupportsCase() + { + var cmd = Database.InsertTyped() + .InsertSql(u => u.Code, u => Sql.Coalesce(Sql.Val("901"), Sql.Val("fallback"))) + .InsertSql(u => u.First, u => Sql.Case().When(Sql.Val(1).Eq(1), "Typed").Else("Other")); + + Assert.AreEqual( + "INSERT INTO \"sample_users\" (\"code\", \"first\") VALUES (COALESCE([$0], [$1]), CASE WHEN ([$2] = [$3]) THEN [$4] ELSE [$5] END)", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestDeleteWhereSql() + { + var cmd = Database.DeleteTyped() + .WhereSql(u => u.Col(x => x.Id).Eq(2).And(u.Col(x => x.Code).NotEq("X"))); + + Assert.AreEqual( + "DELETE FROM \"sample_users\" WHERE ((\"id\" = [$0]) AND (\"code\" <> [$1]))", + NormalizeSql(cmd.CommandText())); + } + } +} diff --git a/DynamORM/Builders/IDynamicTypedDeleteQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedDeleteQueryBuilder.cs index 3ce0b86..d643ea3 100644 --- a/DynamORM/Builders/IDynamicTypedDeleteQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedDeleteQueryBuilder.cs @@ -6,6 +6,7 @@ using System; using System.Linq.Expressions; +using DynamORM.TypedSql; namespace DynamORM.Builders { @@ -17,5 +18,8 @@ namespace DynamORM.Builders /// Predicate to parse. /// Builder instance. IDynamicTypedDeleteQueryBuilder Where(Expression> predicate); + + /// Add typed SQL DSL where predicate. + IDynamicTypedDeleteQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate); } } diff --git a/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs index 2a1f8d4..869eddd 100644 --- a/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedInsertQueryBuilder.cs @@ -6,6 +6,7 @@ using System; using System.Linq.Expressions; +using DynamORM.TypedSql; namespace DynamORM.Builders { @@ -24,5 +25,8 @@ namespace DynamORM.Builders /// Mapped object value. /// Builder instance. IDynamicTypedInsertQueryBuilder Insert(T value); + + /// Add typed SQL DSL insert assignment. + IDynamicTypedInsertQueryBuilder InsertSql(Expression> selector, Func, TypedSqlExpression> valueFactory); } } diff --git a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs index 4a637a5..f165c6c 100644 --- a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs @@ -28,6 +28,7 @@ using System; using System.Linq.Expressions; +using DynamORM.TypedSql; namespace DynamORM.Builders { @@ -89,5 +90,20 @@ namespace DynamORM.Builders /// Additional selectors to parse. /// Builder instance. IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors); + + /// Add typed SQL DSL select items. + IDynamicTypedSelectQueryBuilder SelectSql(Func, TypedSqlSelectable> selector, params Func, TypedSqlSelectable>[] selectors); + + /// Add typed SQL DSL where predicate. + IDynamicTypedSelectQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate); + + /// Add typed SQL DSL having predicate. + IDynamicTypedSelectQueryBuilder HavingSql(Func, TypedSqlPredicate> predicate); + + /// Add typed SQL DSL group by expressions. + IDynamicTypedSelectQueryBuilder GroupBySql(Func, TypedSqlExpression> selector, params Func, TypedSqlExpression>[] selectors); + + /// Add typed SQL DSL order by expressions. + IDynamicTypedSelectQueryBuilder OrderBySql(Func, TypedSqlOrderExpression> selector, params Func, TypedSqlOrderExpression>[] selectors); } } diff --git a/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs index 6285f12..d9842bc 100644 --- a/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedUpdateQueryBuilder.cs @@ -6,6 +6,7 @@ using System; using System.Linq.Expressions; +using DynamORM.TypedSql; namespace DynamORM.Builders { @@ -29,5 +30,11 @@ namespace DynamORM.Builders /// Mapped object value. /// Builder instance. IDynamicTypedUpdateQueryBuilder Values(T value); + + /// Add typed SQL DSL where predicate. + IDynamicTypedUpdateQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate); + + /// Add typed SQL DSL assignment. + IDynamicTypedUpdateQueryBuilder SetSql(Expression> selector, Func, TypedSqlExpression> valueFactory); } } diff --git a/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs index 35d78bf..45aa71d 100644 --- a/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicInsertQueryBuilder.cs @@ -40,8 +40,8 @@ namespace DynamORM.Builders.Implementation /// Implementation of dynamic insert query builder. internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder { - private string _columns; - private string _values; + protected string _columns; + protected string _values; /// /// Initializes a new instance of the class. @@ -221,4 +221,4 @@ namespace DynamORM.Builders.Implementation #endregion IExtendedDisposable } -} \ No newline at end of file +} diff --git a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs index f2bae87..e95dfb4 100644 --- a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs @@ -48,11 +48,11 @@ namespace DynamORM.Builders.Implementation private int? _offset = null; private bool _distinct = false; - private string _select; - private string _from; + protected string _select; + private string _from; protected string _join; - private string _groupby; - private string _orderby; + protected string _groupby; + protected string _orderby; #region IQueryWithHaving diff --git a/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs index f475f95..33403e4 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedDeleteQueryBuilder.cs @@ -7,6 +7,7 @@ using System; using System.Linq.Expressions; using DynamORM.Builders.Extensions; +using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation { @@ -34,6 +35,17 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedDeleteQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) + { + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + public new IDynamicTypedDeleteQueryBuilder Where(Func func) { base.Where(func); @@ -63,5 +75,14 @@ namespace DynamORM.Builders.Implementation base.Where(conditions, schema); return this; } + + private string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return ParseConstant(value, Parameters, columnSchema); + } } } diff --git a/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs index 2ae6ee1..4c087ce 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedInsertQueryBuilder.cs @@ -7,6 +7,7 @@ using System; using System.Linq.Expressions; using DynamORM.Builders.Extensions; +using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation { @@ -40,6 +41,16 @@ namespace DynamORM.Builders.Implementation return this; } + 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); + + _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); @@ -57,5 +68,14 @@ namespace DynamORM.Builders.Implementation base.Insert(o); return this; } + + private string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return ParseConstant(value, Parameters, columnSchema); + } } } diff --git a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs index af340e0..4f681af 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs @@ -34,6 +34,7 @@ using System.Linq.Expressions; using System.Reflection; using DynamORM.Helpers; using DynamORM.Mapper; +using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation { @@ -41,6 +42,45 @@ namespace DynamORM.Builders.Implementation /// Mapped entity type. internal class DynamicTypedSelectQueryBuilder : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder { + private sealed class TypedSqlRenderContext : ITypedSqlRenderContext + { + private readonly DynamicTypedSelectQueryBuilder _builder; + + public TypedSqlRenderContext(DynamicTypedSelectQueryBuilder builder) + { + _builder = builder; + } + + public string ResolveColumn(Type modelType, string memberName, string alias) + { + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType); + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", modelType.FullName)); + + string mappedColumn = mapper.PropertyMap.TryGetValue(memberName) + ?? mapper.PropertyMap.Where(x => string.Equals(x.Key, memberName, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).FirstOrDefault() + ?? memberName; + + return string.IsNullOrEmpty(alias) + ? _builder.Database.DecorateName(mappedColumn) + : string.Format("{0}.{1}", alias, _builder.Database.DecorateName(mappedColumn)); + } + + public string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return _builder.ParseConstant(value, _builder.Parameters, columnSchema); + } + + public string DecorateName(string name) + { + return _builder.Database.DecorateName(name); + } + } + private readonly DynamicTypeMap _mapper; public DynamicTypedSelectQueryBuilder(DynamicDatabase db) @@ -141,7 +181,12 @@ namespace DynamORM.Builders.Implementation if (SupportNoLock && spec.UseNoLock) joinExpr += " WITH(NOLOCK)"; - if (!string.IsNullOrEmpty(spec.OnRawCondition)) + if (spec.OnSqlPredicate != null) + { + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + joinExpr += string.Format(" ON {0}", spec.OnSqlPredicate(new TypedTableContext(GetRootAliasOrTableName()), new TypedTableContext(rightAlias)).Render(context)); + } + else if (!string.IsNullOrEmpty(spec.OnRawCondition)) joinExpr += string.Format(" ON {0}", spec.OnRawCondition); AppendJoinClause(joinExpr); @@ -197,6 +242,19 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedSelectQueryBuilder SelectSql(Func, TypedSqlSelectable> selector, params Func, TypedSqlSelectable>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddSelectSqlSelector(selector); + if (selectors != null) + foreach (Func, TypedSqlSelectable> item in selectors) + AddSelectSqlSelector(item); + + return this; + } + public IDynamicTypedSelectQueryBuilder GroupBy(Expression> selector, params Expression>[] selectors) { if (selector == null) @@ -216,6 +274,19 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedSelectQueryBuilder GroupBySql(Func, TypedSqlExpression> selector, params Func, TypedSqlExpression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddGroupBySqlSelector(selector); + if (selectors != null) + foreach (Func, TypedSqlExpression> item in selectors) + AddGroupBySqlSelector(item); + + return this; + } + public IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors) { if (selector == null) @@ -235,6 +306,19 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedSelectQueryBuilder OrderBySql(Func, TypedSqlOrderExpression> selector, params Func, TypedSqlOrderExpression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddOrderBySqlSelector(selector); + if (selectors != null) + foreach (Func, TypedSqlOrderExpression> item in selectors) + AddOrderBySqlSelector(item); + + return this; + } + public new IDynamicTypedSelectQueryBuilder Top(int? top) { base.Top(top); @@ -274,6 +358,34 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedSelectQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = RenderSqlPredicate(predicate); + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + + public IDynamicTypedSelectQueryBuilder HavingSql(Func, TypedSqlPredicate> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = RenderSqlPredicate(predicate); + if (string.IsNullOrEmpty(HavingCondition)) + HavingCondition = condition; + else + HavingCondition = string.Format("{0} AND {1}", HavingCondition, condition); + + return this; + } + public new IDynamicTypedSelectQueryBuilder Having(DynamicColumn column) { base.Having(column); @@ -317,6 +429,17 @@ namespace DynamORM.Builders.Implementation } } + private void AddSelectSqlSelector(Func, TypedSqlSelectable> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + TypedSqlSelectable item = selector(new TypedTableContext(GetRootAliasOrTableName())); + string rendered = item.Render(context); + _select = string.IsNullOrEmpty(_select) ? rendered : string.Format("{0}, {1}", _select, rendered); + } + private void AddGroupBySelector(Expression> selector) { var body = UnwrapConvert(selector.Body); @@ -336,6 +459,17 @@ namespace DynamORM.Builders.Implementation } } + private void AddGroupBySqlSelector(Func, TypedSqlExpression> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + TypedSqlExpression item = selector(new TypedTableContext(GetRootAliasOrTableName())); + string rendered = item.Render(context); + _groupby = string.IsNullOrEmpty(_groupby) ? rendered : string.Format("{0}, {1}", _groupby, rendered); + } + private void AddOrderBySelector(Expression> selector) { var body = UnwrapConvert(selector.Body); @@ -352,6 +486,23 @@ namespace DynamORM.Builders.Implementation ((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed); } + private void AddOrderBySqlSelector(Func, TypedSqlOrderExpression> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + TypedSqlOrderExpression item = selector(new TypedTableContext(GetRootAliasOrTableName())); + string rendered = item.Render(context); + _orderby = string.IsNullOrEmpty(_orderby) ? rendered : string.Format("{0}, {1}", _orderby, rendered); + } + + private string RenderSqlPredicate(Func, TypedSqlPredicate> predicate) + { + TypedSqlRenderContext context = new TypedSqlRenderContext(this); + return predicate(new TypedTableContext(GetRootAliasOrTableName())).Render(context); + } + private string ParseTypedCondition(Expression expression) { expression = UnwrapConvert(expression); diff --git a/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs index 6b6720d..2fd2752 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedUpdateQueryBuilder.cs @@ -7,6 +7,7 @@ using System; using System.Linq.Expressions; using DynamORM.Builders.Extensions; +using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation { @@ -46,6 +47,26 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedUpdateQueryBuilder WhereSql(Func, TypedSqlPredicate> predicate) + { + string condition = TypedModifyHelper.RenderPredicate(predicate, null, RenderValue, Database.DecorateName); + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + + 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 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); @@ -105,5 +126,14 @@ namespace DynamORM.Builders.Implementation base.Where(conditions, schema); return this; } + + private string RenderValue(object value) + { + if (value == null) + return "NULL"; + + DynamicSchemaColumn? columnSchema = null; + return ParseConstant(value, Parameters, columnSchema); + } } } diff --git a/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs index 030382c..f329755 100644 --- a/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicUpdateQueryBuilder.cs @@ -41,7 +41,7 @@ namespace DynamORM.Builders.Implementation /// Update query builder. internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere { - private string _columns; + protected string _columns; internal DynamicUpdateQueryBuilder(DynamicDatabase db) : base(db) @@ -339,4 +339,4 @@ namespace DynamORM.Builders.Implementation #endregion IExtendedDisposable } -} \ No newline at end of file +} diff --git a/DynamORM/Builders/Implementation/TypedModifyHelper.cs b/DynamORM/Builders/Implementation/TypedModifyHelper.cs index 8625026..a75309a 100644 --- a/DynamORM/Builders/Implementation/TypedModifyHelper.cs +++ b/DynamORM/Builders/Implementation/TypedModifyHelper.cs @@ -5,15 +5,46 @@ */ using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using DynamORM.Mapper; +using DynamORM.TypedSql; namespace DynamORM.Builders.Implementation { /// Helper methods for typed modify builders. internal static class TypedModifyHelper { + private sealed class ModifyRenderContext : ITypedSqlRenderContext + { + private readonly Func, string> _resolveColumn; + private readonly Func _renderValue; + private readonly Func _decorateName; + + public ModifyRenderContext(Func, string> resolveColumn, Func renderValue, Func decorateName) + { + _resolveColumn = resolveColumn; + _renderValue = renderValue; + _decorateName = decorateName; + } + + public string ResolveColumn(Type modelType, string memberName, string alias) + { + return _resolveColumn(modelType, memberName, alias, _decorateName); + } + + public string RenderValue(object value) + { + return _renderValue(value); + } + + public string DecorateName(string name) + { + return _decorateName(name); + } + } + public static string GetMappedColumn(Expression> selector) { if (selector == null) @@ -32,6 +63,32 @@ namespace DynamORM.Builders.Implementation ApplyWhereInternal(typeof(T), addCondition, predicate.Body); } + public static string RenderPredicate( + Func, TypedSqlPredicate> predicate, + string alias, + Func renderValue, + Func decorateName) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + return predicate(new TypedTableContext(alias)).Render(context); + } + + public static string RenderExpression( + Func, TypedSqlExpression> expression, + string alias, + Func renderValue, + Func decorateName) + { + if (expression == null) + throw new ArgumentNullException("expression"); + + ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName); + return expression(new TypedTableContext(alias)).Render(context); + } + private static void ApplyWhereInternal(Type modelType, Action addCondition, Expression expression) { expression = UnwrapConvert(expression); @@ -93,6 +150,25 @@ namespace DynamORM.Builders.Implementation ?? member.Member.Name; } + private static string ResolveColumn(Type modelType, string memberName, string alias, Func decorateName) + { + string mapped = GetMappedColumnByName(modelType, memberName); + return string.IsNullOrEmpty(alias) + ? decorateName(mapped) + : string.Format("{0}.{1}", alias, decorateName(mapped)); + } + + private static string GetMappedColumnByName(Type modelType, string memberName) + { + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType); + if (mapper == null) + throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", modelType.FullName)); + + return mapper.PropertyMap.TryGetValue(memberName) + ?? mapper.PropertyMap.Where(x => string.Equals(x.Key, memberName, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).FirstOrDefault() + ?? memberName; + } + private static Expression UnwrapConvert(Expression expression) { while (expression is UnaryExpression && diff --git a/DynamORM/Builders/TypedJoinBuilder.cs b/DynamORM/Builders/TypedJoinBuilder.cs index ac0440b..a0ae403 100644 --- a/DynamORM/Builders/TypedJoinBuilder.cs +++ b/DynamORM/Builders/TypedJoinBuilder.cs @@ -6,6 +6,7 @@ using System; using System.Linq.Expressions; +using DynamORM.TypedSql; namespace DynamORM.Builders { @@ -37,6 +38,9 @@ namespace DynamORM.Builders /// Gets raw ON condition. public string OnRawCondition { get; private set; } + /// Gets typed SQL DSL ON specification. + public Func, TypedTableContext, TypedSqlPredicate> OnSqlPredicate { get; private set; } + /// Sets join alias. public TypedJoinBuilder As(string alias) { @@ -133,6 +137,7 @@ namespace DynamORM.Builders OnPredicate = predicate; OnRawCondition = null; + OnSqlPredicate = null; return this; } @@ -144,6 +149,19 @@ namespace DynamORM.Builders OnRawCondition = condition.Trim(); OnPredicate = null; + OnSqlPredicate = null; + return this; + } + + /// Sets typed SQL DSL ON predicate. + public TypedJoinBuilder OnSql(Func, TypedTableContext, TypedSqlPredicate> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + OnSqlPredicate = predicate; + OnPredicate = null; + OnRawCondition = null; return this; } } diff --git a/DynamORM/TypedSql/ITypedSqlRenderContext.cs b/DynamORM/TypedSql/ITypedSqlRenderContext.cs new file mode 100644 index 0000000..58ed69d --- /dev/null +++ b/DynamORM/TypedSql/ITypedSqlRenderContext.cs @@ -0,0 +1,23 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System; + +namespace DynamORM.TypedSql +{ + /// Render context used by typed SQL DSL nodes. + public interface ITypedSqlRenderContext + { + /// Resolve mapped column for given model member. + string ResolveColumn(Type modelType, string memberName, string alias); + + /// Render value as SQL parameter or literal fragment. + string RenderValue(object value); + + /// Decorate SQL identifier. + string DecorateName(string name); + } +} diff --git a/DynamORM/TypedSql/Sql.cs b/DynamORM/TypedSql/Sql.cs new file mode 100644 index 0000000..91f46d5 --- /dev/null +++ b/DynamORM/TypedSql/Sql.cs @@ -0,0 +1,91 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System; +using System.Collections.Generic; + +namespace DynamORM.TypedSql +{ + /// Entry point for the typed SQL DSL. + public static class Sql + { + /// Create parameterized value expression. + public static TypedSqlExpression Val(T value) + { + return new TypedSqlValueExpression(value); + } + + /// Create parameterized value expression. + public static TypedSqlExpression Val(object value) + { + return new TypedSqlValueExpression(value); + } + + /// Create raw SQL expression. + public static TypedSqlExpression Raw(string sql) + { + return new TypedSqlRawExpression(sql); + } + + /// Create generic function call. + public static TypedSqlExpression Func(string name, params TypedSqlExpression[] arguments) + { + return new TypedSqlFunctionExpression(name, arguments); + } + + /// Create COUNT(*) expression. + public static TypedSqlExpression Count() + { + return Raw("COUNT(*)"); + } + + /// Create COUNT(expr) expression. + public static TypedSqlExpression Count(TypedSqlExpression expression) + { + return Func("COUNT", expression); + } + + /// Create COALESCE expression. + public static TypedSqlExpression Coalesce(params TypedSqlExpression[] expressions) + { + return Func("COALESCE", expressions); + } + + /// Create CASE expression builder. + public static TypedSqlCaseBuilder Case() + { + return new TypedSqlCaseBuilder(); + } + } + + /// Builder for CASE expressions. + /// Result type. + public sealed class TypedSqlCaseBuilder + { + private readonly IList> _cases = new List>(); + + /// Add WHEN ... THEN ... clause. + public TypedSqlCaseBuilder When(TypedSqlPredicate predicate, object value) + { + _cases.Add(new KeyValuePair( + predicate, + value as TypedSqlExpression ?? Sql.Val(value))); + return this; + } + + /// Finalize CASE expression with ELSE clause. + public TypedSqlExpression Else(object value) + { + return new TypedSqlCaseExpression(_cases, value as TypedSqlExpression ?? Sql.Val(value)); + } + + /// Finalize CASE expression without ELSE clause. + public TypedSqlExpression End() + { + return new TypedSqlCaseExpression(_cases, null); + } + } +} diff --git a/DynamORM/TypedSql/TypedSqlExpression.cs b/DynamORM/TypedSql/TypedSqlExpression.cs new file mode 100644 index 0000000..7676013 --- /dev/null +++ b/DynamORM/TypedSql/TypedSqlExpression.cs @@ -0,0 +1,321 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System; +using System.Collections.Generic; + +namespace DynamORM.TypedSql +{ + /// Base selectable SQL fragment for the typed DSL. + public abstract class TypedSqlSelectable + { + internal abstract string Render(ITypedSqlRenderContext context); + } + + /// Base SQL expression for the typed DSL. + public abstract class TypedSqlExpression : TypedSqlSelectable + { + /// Alias this expression in SELECT clause. + public TypedSqlAliasedExpression As(string alias) + { + return new TypedSqlAliasedExpression(this, alias); + } + + /// Order ascending. + public TypedSqlOrderExpression Asc() + { + return new TypedSqlOrderExpression(this, true); + } + + /// Order descending. + public TypedSqlOrderExpression Desc() + { + return new TypedSqlOrderExpression(this, false); + } + + /// Equality predicate. + public TypedSqlPredicate Eq(object value) + { + return new TypedSqlBinaryPredicate(this, "=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + + /// Inequality predicate. + public TypedSqlPredicate NotEq(object value) + { + return new TypedSqlBinaryPredicate(this, "<>", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + + /// Greater-than predicate. + public TypedSqlPredicate Gt(object value) + { + return new TypedSqlBinaryPredicate(this, ">", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + + /// Greater-than-or-equal predicate. + public TypedSqlPredicate Gte(object value) + { + return new TypedSqlBinaryPredicate(this, ">=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + + /// Less-than predicate. + public TypedSqlPredicate Lt(object value) + { + return new TypedSqlBinaryPredicate(this, "<", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + + /// Less-than-or-equal predicate. + public TypedSqlPredicate Lte(object value) + { + return new TypedSqlBinaryPredicate(this, "<=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value)); + } + + /// IS NULL predicate. + public TypedSqlPredicate IsNull() + { + return new TypedSqlUnaryPredicate(this, "IS NULL"); + } + + /// IS NOT NULL predicate. + public TypedSqlPredicate IsNotNull() + { + return new TypedSqlUnaryPredicate(this, "IS NOT NULL"); + } + } + + /// Typed SQL expression. + public abstract class TypedSqlExpression : TypedSqlExpression + { + } + + /// Typed SQL predicate expression. + public abstract class TypedSqlPredicate : TypedSqlExpression + { + /// Combine with AND. + public TypedSqlPredicate And(TypedSqlPredicate right) + { + return new TypedSqlCombinedPredicate(this, "AND", right); + } + + /// Combine with OR. + public TypedSqlPredicate Or(TypedSqlPredicate right) + { + return new TypedSqlCombinedPredicate(this, "OR", right); + } + + /// Negate predicate. + public TypedSqlPredicate Not() + { + return new TypedSqlNegatedPredicate(this); + } + } + + /// Aliased SQL expression. + public sealed class TypedSqlAliasedExpression : TypedSqlSelectable + { + private readonly TypedSqlExpression _expression; + private readonly string _alias; + + internal TypedSqlAliasedExpression(TypedSqlExpression expression, string alias) + { + _expression = expression; + _alias = alias; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("{0} AS {1}", _expression.Render(context), context.DecorateName(_alias)); + } + } + + /// Ordered SQL expression. + public sealed class TypedSqlOrderExpression : TypedSqlSelectable + { + private readonly TypedSqlExpression _expression; + private readonly bool _ascending; + + internal TypedSqlOrderExpression(TypedSqlExpression expression, bool ascending) + { + _expression = expression; + _ascending = ascending; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("{0} {1}", _expression.Render(context), _ascending ? "ASC" : "DESC"); + } + } + + internal sealed class TypedSqlColumnExpression : TypedSqlExpression + { + private readonly Type _modelType; + private readonly string _memberName; + private readonly string _alias; + + internal TypedSqlColumnExpression(Type modelType, string memberName, string alias) + { + _modelType = modelType; + _memberName = memberName; + _alias = alias; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return context.ResolveColumn(_modelType, _memberName, _alias); + } + } + + internal sealed class TypedSqlValueExpression : TypedSqlExpression + { + private readonly object _value; + + internal TypedSqlValueExpression(object value) + { + _value = value; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return context.RenderValue(_value); + } + } + + internal sealed class TypedSqlRawExpression : TypedSqlExpression + { + private readonly string _sql; + + internal TypedSqlRawExpression(string sql) + { + _sql = sql; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return _sql; + } + } + + internal sealed class TypedSqlFunctionExpression : TypedSqlExpression + { + private readonly string _name; + private readonly IList _arguments; + + internal TypedSqlFunctionExpression(string name, params TypedSqlExpression[] arguments) + { + _name = name; + _arguments = arguments ?? new TypedSqlExpression[0]; + } + + internal override string Render(ITypedSqlRenderContext context) + { + List rendered = new List(); + foreach (TypedSqlExpression argument in _arguments) + rendered.Add(argument.Render(context)); + + return string.Format("{0}({1})", _name, string.Join(", ", rendered.ToArray())); + } + } + + internal sealed class TypedSqlUnaryPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _expression; + private readonly string _operator; + + internal TypedSqlUnaryPredicate(TypedSqlExpression expression, string op) + { + _expression = expression; + _operator = op; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0} {1})", _expression.Render(context), _operator); + } + } + + internal sealed class TypedSqlBinaryPredicate : TypedSqlPredicate + { + private readonly TypedSqlExpression _left; + private readonly string _operator; + private readonly TypedSqlExpression _right; + + internal TypedSqlBinaryPredicate(TypedSqlExpression left, string op, TypedSqlExpression right) + { + _left = left; + _operator = op; + _right = right; + } + + internal override string Render(ITypedSqlRenderContext context) + { + string op = _operator; + if (_right is TypedSqlValueExpression && string.Equals(_right.Render(context), "NULL", StringComparison.OrdinalIgnoreCase)) + op = _operator == "=" ? "IS" : _operator == "<>" ? "IS NOT" : _operator; + + return string.Format("({0} {1} {2})", _left.Render(context), op, _right.Render(context)); + } + } + + internal sealed class TypedSqlCombinedPredicate : TypedSqlPredicate + { + private readonly TypedSqlPredicate _left; + private readonly string _operator; + private readonly TypedSqlPredicate _right; + + internal TypedSqlCombinedPredicate(TypedSqlPredicate left, string op, TypedSqlPredicate right) + { + _left = left; + _operator = op; + _right = right; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("({0} {1} {2})", _left.Render(context), _operator, _right.Render(context)); + } + } + + internal sealed class TypedSqlNegatedPredicate : TypedSqlPredicate + { + private readonly TypedSqlPredicate _predicate; + + internal TypedSqlNegatedPredicate(TypedSqlPredicate predicate) + { + _predicate = predicate; + } + + internal override string Render(ITypedSqlRenderContext context) + { + return string.Format("(NOT {0})", _predicate.Render(context)); + } + } + + internal sealed class TypedSqlCaseExpression : TypedSqlExpression + { + private readonly IList> _cases; + private readonly TypedSqlExpression _elseExpression; + + internal TypedSqlCaseExpression(IList> cases, TypedSqlExpression elseExpression) + { + _cases = cases; + _elseExpression = elseExpression; + } + + internal override string Render(ITypedSqlRenderContext context) + { + List items = new List(); + items.Add("CASE"); + + foreach (KeyValuePair item in _cases) + items.Add(string.Format("WHEN {0} THEN {1}", item.Key.Render(context), item.Value.Render(context))); + + if (_elseExpression != null) + items.Add(string.Format("ELSE {0}", _elseExpression.Render(context))); + + items.Add("END"); + return string.Join(" ", items.ToArray()); + } + } +} diff --git a/DynamORM/TypedSql/TypedTableContext.cs b/DynamORM/TypedSql/TypedTableContext.cs new file mode 100644 index 0000000..39adad9 --- /dev/null +++ b/DynamORM/TypedSql/TypedTableContext.cs @@ -0,0 +1,40 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System; +using System.Linq.Expressions; + +namespace DynamORM.TypedSql +{ + /// Typed table context used by the typed SQL DSL. + /// Mapped entity type. + public sealed class TypedTableContext + { + internal TypedTableContext(string alias) + { + Alias = alias; + } + + /// Gets table alias used by the current query. + public string Alias { get; private set; } + + /// Creates a mapped column expression. + public TypedSqlExpression Col(Expression> selector) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + MemberExpression member = selector.Body as MemberExpression; + if (member == null && selector.Body is UnaryExpression) + member = ((UnaryExpression)selector.Body).Operand as MemberExpression; + + if (member == null) + throw new NotSupportedException(string.Format("Column selector must target a mapped property: {0}", selector)); + + return new TypedSqlColumnExpression(typeof(T), member.Member.Name, Alias); + } + } +}