Add experimental typed SQL DSL for typed builders

This commit is contained in:
root
2026-02-27 08:22:23 +01:00
parent 3d430b0124
commit 8b1737a53b
19 changed files with 1649 additions and 17 deletions

View File

@@ -10,6 +10,7 @@ using DynamORM.Builders;
using DynamORM.Helpers.Dynamics; using DynamORM.Helpers.Dynamics;
using DynamORM.Helpers; using DynamORM.Helpers;
using DynamORM.Mapper; using DynamORM.Mapper;
using DynamORM.TypedSql;
using DynamORM.Validation; using DynamORM.Validation;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@@ -7203,6 +7204,9 @@ namespace DynamORM
/// <param name="predicate">Predicate to parse.</param> /// <param name="predicate">Predicate to parse.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedDeleteQueryBuilder<T> Where(Expression<Func<T, bool>> predicate); IDynamicTypedDeleteQueryBuilder<T> Where(Expression<Func<T, bool>> predicate);
/// <summary>Add typed SQL DSL where predicate.</summary>
IDynamicTypedDeleteQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
} }
/// <summary>Typed insert query builder for mapped entities.</summary> /// <summary>Typed insert query builder for mapped entities.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam> /// <typeparam name="T">Mapped entity type.</typeparam>
@@ -7219,6 +7223,9 @@ namespace DynamORM
/// <param name="value">Mapped object value.</param> /// <param name="value">Mapped object value.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedInsertQueryBuilder<T> Insert(T value); IDynamicTypedInsertQueryBuilder<T> Insert(T value);
/// <summary>Add typed SQL DSL insert assignment.</summary>
IDynamicTypedInsertQueryBuilder<T> InsertSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, TypedSqlExpression> valueFactory);
} }
/// <summary>Typed select query builder for mapped entities.</summary> /// <summary>Typed select query builder for mapped entities.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam> /// <typeparam name="T">Mapped entity type.</typeparam>
@@ -7278,6 +7285,21 @@ namespace DynamORM
/// <param name="selectors">Additional selectors to parse.</param> /// <param name="selectors">Additional selectors to parse.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors); IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors);
/// <summary>Add typed SQL DSL select items.</summary>
IDynamicTypedSelectQueryBuilder<T> SelectSql(Func<TypedTableContext<T>, TypedSqlSelectable> selector, params Func<TypedTableContext<T>, TypedSqlSelectable>[] selectors);
/// <summary>Add typed SQL DSL where predicate.</summary>
IDynamicTypedSelectQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
/// <summary>Add typed SQL DSL having predicate.</summary>
IDynamicTypedSelectQueryBuilder<T> HavingSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
/// <summary>Add typed SQL DSL group by expressions.</summary>
IDynamicTypedSelectQueryBuilder<T> GroupBySql(Func<TypedTableContext<T>, TypedSqlExpression> selector, params Func<TypedTableContext<T>, TypedSqlExpression>[] selectors);
/// <summary>Add typed SQL DSL order by expressions.</summary>
IDynamicTypedSelectQueryBuilder<T> OrderBySql(Func<TypedTableContext<T>, TypedSqlOrderExpression> selector, params Func<TypedTableContext<T>, TypedSqlOrderExpression>[] selectors);
} }
/// <summary>Typed update query builder for mapped entities.</summary> /// <summary>Typed update query builder for mapped entities.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam> /// <typeparam name="T">Mapped entity type.</typeparam>
@@ -7299,6 +7321,12 @@ namespace DynamORM
/// <param name="value">Mapped object value.</param> /// <param name="value">Mapped object value.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedUpdateQueryBuilder<T> Values(T value); IDynamicTypedUpdateQueryBuilder<T> Values(T value);
/// <summary>Add typed SQL DSL where predicate.</summary>
IDynamicTypedUpdateQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
/// <summary>Add typed SQL DSL assignment.</summary>
IDynamicTypedUpdateQueryBuilder<T> SetSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, TypedSqlExpression> valueFactory);
} }
/// <summary>Dynamic update query builder interface.</summary> /// <summary>Dynamic update query builder interface.</summary>
/// <remarks>This interface it publicly available. Implementation should be hidden.</remarks> /// <remarks>This interface it publicly available. Implementation should be hidden.</remarks>
@@ -7475,6 +7503,9 @@ namespace DynamORM
/// <summary>Gets raw ON condition.</summary> /// <summary>Gets raw ON condition.</summary>
public string OnRawCondition { get; private set; } public string OnRawCondition { get; private set; }
/// <summary>Gets typed SQL DSL ON specification.</summary>
public Func<TypedTableContext<TLeft>, TypedTableContext<TRight>, TypedSqlPredicate> OnSqlPredicate { get; private set; }
/// <summary>Sets join alias.</summary> /// <summary>Sets join alias.</summary>
public TypedJoinBuilder<TLeft, TRight> As(string alias) public TypedJoinBuilder<TLeft, TRight> As(string alias)
{ {
@@ -7560,6 +7591,7 @@ namespace DynamORM
OnPredicate = predicate; OnPredicate = predicate;
OnRawCondition = null; OnRawCondition = null;
OnSqlPredicate = null;
return this; return this;
} }
/// <summary>Sets raw ON clause (without the ON keyword).</summary> /// <summary>Sets raw ON clause (without the ON keyword).</summary>
@@ -7570,6 +7602,18 @@ namespace DynamORM
OnRawCondition = condition.Trim(); OnRawCondition = condition.Trim();
OnPredicate = null; OnPredicate = null;
OnSqlPredicate = null;
return this;
}
/// <summary>Sets typed SQL DSL ON predicate.</summary>
public TypedJoinBuilder<TLeft, TRight> OnSql(Func<TypedTableContext<TLeft>, TypedTableContext<TRight>, TypedSqlPredicate> predicate)
{
if (predicate == null)
throw new ArgumentNullException("predicate");
OnSqlPredicate = predicate;
OnPredicate = null;
OnRawCondition = null;
return this; return this;
} }
} }
@@ -8229,8 +8273,8 @@ namespace DynamORM
/// <summary>Implementation of dynamic insert query builder.</summary> /// <summary>Implementation of dynamic insert query builder.</summary>
internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder
{ {
private string _columns; protected string _columns;
private string _values; protected string _values;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DynamicInsertQueryBuilder"/> class. /// Initializes a new instance of the <see cref="DynamicInsertQueryBuilder"/> class.
@@ -9338,11 +9382,11 @@ namespace DynamORM
private int? _offset = null; private int? _offset = null;
private bool _distinct = false; private bool _distinct = false;
private string _select; protected string _select;
private string _from; private string _from;
protected string _join; protected string _join;
private string _groupby; protected string _groupby;
private string _orderby; protected string _orderby;
#region IQueryWithHaving #region IQueryWithHaving
@@ -10678,6 +10722,16 @@ namespace DynamORM
TypedModifyHelper.ApplyWhere<T>((c, o, v) => base.Where(c, o, v), predicate); TypedModifyHelper.ApplyWhere<T>((c, o, v) => base.Where(c, o, v), predicate);
return this; return this;
} }
public IDynamicTypedDeleteQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, 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<T> Where(Func<dynamic, object> func) public new IDynamicTypedDeleteQueryBuilder<T> Where(Func<dynamic, object> func)
{ {
base.Where(func); base.Where(func);
@@ -10703,6 +10757,14 @@ namespace DynamORM
base.Where(conditions, schema); base.Where(conditions, schema);
return this; return this;
} }
private string RenderValue(object value)
{
if (value == null)
return "NULL";
DynamicSchemaColumn? columnSchema = null;
return ParseConstant(value, Parameters, columnSchema);
}
} }
/// <summary>Typed wrapper over <see cref="DynamicInsertQueryBuilder"/> with property-to-column translation.</summary> /// <summary>Typed wrapper over <see cref="DynamicInsertQueryBuilder"/> with property-to-column translation.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam> /// <typeparam name="T">Mapped entity type.</typeparam>
@@ -10730,6 +10792,15 @@ namespace DynamORM
base.Insert(value); base.Insert(value);
return this; return this;
} }
public IDynamicTypedInsertQueryBuilder<T> InsertSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, 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<T> Values(Func<dynamic, object> fn, params Func<dynamic, object>[] func) public new IDynamicTypedInsertQueryBuilder<T> Values(Func<dynamic, object> fn, params Func<dynamic, object>[] func)
{ {
base.Values(fn, func); base.Values(fn, func);
@@ -10745,11 +10816,54 @@ namespace DynamORM
base.Insert(o); base.Insert(o);
return this; return this;
} }
private string RenderValue(object value)
{
if (value == null)
return "NULL";
DynamicSchemaColumn? columnSchema = null;
return ParseConstant(value, Parameters, columnSchema);
}
} }
/// <summary>Typed wrapper over <see cref="DynamicSelectQueryBuilder"/> with property-to-column translation.</summary> /// <summary>Typed wrapper over <see cref="DynamicSelectQueryBuilder"/> with property-to-column translation.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam> /// <typeparam name="T">Mapped entity type.</typeparam>
internal class DynamicTypedSelectQueryBuilder<T> : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder<T> internal class DynamicTypedSelectQueryBuilder<T> : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder<T>
{ {
private sealed class TypedSqlRenderContext : ITypedSqlRenderContext
{
private readonly DynamicTypedSelectQueryBuilder<T> _builder;
public TypedSqlRenderContext(DynamicTypedSelectQueryBuilder<T> 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; private readonly DynamicTypeMap _mapper;
public DynamicTypedSelectQueryBuilder(DynamicDatabase db) public DynamicTypedSelectQueryBuilder(DynamicDatabase db)
@@ -10846,7 +10960,12 @@ namespace DynamORM
if (SupportNoLock && spec.UseNoLock) if (SupportNoLock && spec.UseNoLock)
joinExpr += " WITH(NOLOCK)"; 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<T>(GetRootAliasOrTableName()), new TypedTableContext<TRight>(rightAlias)).Render(context));
}
else if (!string.IsNullOrEmpty(spec.OnRawCondition))
joinExpr += string.Format(" ON {0}", spec.OnRawCondition); joinExpr += string.Format(" ON {0}", spec.OnRawCondition);
AppendJoinClause(joinExpr); AppendJoinClause(joinExpr);
@@ -10894,6 +11013,18 @@ namespace DynamORM
} }
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> SelectSql(Func<TypedTableContext<T>, TypedSqlSelectable> selector, params Func<TypedTableContext<T>, TypedSqlSelectable>[] selectors)
{
if (selector == null)
throw new ArgumentNullException("selector");
AddSelectSqlSelector(selector);
if (selectors != null)
foreach (Func<TypedTableContext<T>, TypedSqlSelectable> item in selectors)
AddSelectSqlSelector(item);
return this;
}
public IDynamicTypedSelectQueryBuilder<T> GroupBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors) public IDynamicTypedSelectQueryBuilder<T> GroupBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors)
{ {
if (selector == null) if (selector == null)
@@ -10911,6 +11042,18 @@ namespace DynamORM
} }
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> GroupBySql(Func<TypedTableContext<T>, TypedSqlExpression> selector, params Func<TypedTableContext<T>, TypedSqlExpression>[] selectors)
{
if (selector == null)
throw new ArgumentNullException("selector");
AddGroupBySqlSelector(selector);
if (selectors != null)
foreach (Func<TypedTableContext<T>, TypedSqlExpression> item in selectors)
AddGroupBySqlSelector(item);
return this;
}
public IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors) public IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors)
{ {
if (selector == null) if (selector == null)
@@ -10928,6 +11071,18 @@ namespace DynamORM
} }
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> OrderBySql(Func<TypedTableContext<T>, TypedSqlOrderExpression> selector, params Func<TypedTableContext<T>, TypedSqlOrderExpression>[] selectors)
{
if (selector == null)
throw new ArgumentNullException("selector");
AddOrderBySqlSelector(selector);
if (selectors != null)
foreach (Func<TypedTableContext<T>, TypedSqlOrderExpression> item in selectors)
AddOrderBySqlSelector(item);
return this;
}
public new IDynamicTypedSelectQueryBuilder<T> Top(int? top) public new IDynamicTypedSelectQueryBuilder<T> Top(int? top)
{ {
base.Top(top); base.Top(top);
@@ -10962,6 +11117,32 @@ namespace DynamORM
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, 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<T> HavingSql(Func<TypedTableContext<T>, 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<T> Having(DynamicColumn column) public new IDynamicTypedSelectQueryBuilder<T> Having(DynamicColumn column)
{ {
base.Having(column); base.Having(column);
@@ -11000,6 +11181,16 @@ namespace DynamORM
((IDynamicSelectQueryBuilder)this).Select(x => parsed); ((IDynamicSelectQueryBuilder)this).Select(x => parsed);
} }
} }
private void AddSelectSqlSelector(Func<TypedTableContext<T>, TypedSqlSelectable> selector)
{
if (selector == null)
throw new ArgumentNullException("selector");
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
TypedSqlSelectable item = selector(new TypedTableContext<T>(GetRootAliasOrTableName()));
string rendered = item.Render(context);
_select = string.IsNullOrEmpty(_select) ? rendered : string.Format("{0}, {1}", _select, rendered);
}
private void AddGroupBySelector<TResult>(Expression<Func<T, TResult>> selector) private void AddGroupBySelector<TResult>(Expression<Func<T, TResult>> selector)
{ {
var body = UnwrapConvert(selector.Body); var body = UnwrapConvert(selector.Body);
@@ -11018,6 +11209,16 @@ namespace DynamORM
((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed); ((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed);
} }
} }
private void AddGroupBySqlSelector(Func<TypedTableContext<T>, TypedSqlExpression> selector)
{
if (selector == null)
throw new ArgumentNullException("selector");
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
TypedSqlExpression item = selector(new TypedTableContext<T>(GetRootAliasOrTableName()));
string rendered = item.Render(context);
_groupby = string.IsNullOrEmpty(_groupby) ? rendered : string.Format("{0}, {1}", _groupby, rendered);
}
private void AddOrderBySelector<TResult>(Expression<Func<T, TResult>> selector) private void AddOrderBySelector<TResult>(Expression<Func<T, TResult>> selector)
{ {
var body = UnwrapConvert(selector.Body); var body = UnwrapConvert(selector.Body);
@@ -11032,6 +11233,21 @@ namespace DynamORM
string parsed = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); string parsed = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC");
((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed); ((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed);
} }
private void AddOrderBySqlSelector(Func<TypedTableContext<T>, TypedSqlOrderExpression> selector)
{
if (selector == null)
throw new ArgumentNullException("selector");
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
TypedSqlOrderExpression item = selector(new TypedTableContext<T>(GetRootAliasOrTableName()));
string rendered = item.Render(context);
_orderby = string.IsNullOrEmpty(_orderby) ? rendered : string.Format("{0}, {1}", _orderby, rendered);
}
private string RenderSqlPredicate(Func<TypedTableContext<T>, TypedSqlPredicate> predicate)
{
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
return predicate(new TypedTableContext<T>(GetRootAliasOrTableName())).Render(context);
}
private string ParseTypedCondition(Expression expression) private string ParseTypedCondition(Expression expression)
{ {
expression = UnwrapConvert(expression); expression = UnwrapConvert(expression);
@@ -11415,6 +11631,24 @@ namespace DynamORM
base.Values(value); base.Values(value);
return this; return this;
} }
public IDynamicTypedUpdateQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, 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<T> SetSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, 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<T> Update(string column, object value) public new IDynamicTypedUpdateQueryBuilder<T> Update(string column, object value)
{ {
base.Update(column, value); base.Update(column, value);
@@ -11465,11 +11699,19 @@ namespace DynamORM
base.Where(conditions, schema); base.Where(conditions, schema);
return this; return this;
} }
private string RenderValue(object value)
{
if (value == null)
return "NULL";
DynamicSchemaColumn? columnSchema = null;
return ParseConstant(value, Parameters, columnSchema);
}
} }
/// <summary>Update query builder.</summary> /// <summary>Update query builder.</summary>
internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere
{ {
private string _columns; protected string _columns;
internal DynamicUpdateQueryBuilder(DynamicDatabase db) internal DynamicUpdateQueryBuilder(DynamicDatabase db)
: base(db) : base(db)
@@ -11749,6 +11991,31 @@ namespace DynamORM
/// <summary>Helper methods for typed modify builders.</summary> /// <summary>Helper methods for typed modify builders.</summary>
internal static class TypedModifyHelper internal static class TypedModifyHelper
{ {
private sealed class ModifyRenderContext : ITypedSqlRenderContext
{
private readonly Func<Type, string, string, Func<string, string>, string> _resolveColumn;
private readonly Func<object, string> _renderValue;
private readonly Func<string, string> _decorateName;
public ModifyRenderContext(Func<Type, string, string, Func<string, string>, string> resolveColumn, Func<object, string> renderValue, Func<string, string> 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<T, TValue>(Expression<Func<T, TValue>> selector) public static string GetMappedColumn<T, TValue>(Expression<Func<T, TValue>> selector)
{ {
if (selector == null) if (selector == null)
@@ -11765,6 +12032,30 @@ namespace DynamORM
ApplyWhereInternal(typeof(T), addCondition, predicate.Body); ApplyWhereInternal(typeof(T), addCondition, predicate.Body);
} }
public static string RenderPredicate<T>(
Func<TypedTableContext<T>, TypedSqlPredicate> predicate,
string alias,
Func<object, string> renderValue,
Func<string, string> decorateName)
{
if (predicate == null)
throw new ArgumentNullException("predicate");
ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName);
return predicate(new TypedTableContext<T>(alias)).Render(context);
}
public static string RenderExpression<T>(
Func<TypedTableContext<T>, TypedSqlExpression> expression,
string alias,
Func<object, string> renderValue,
Func<string, string> decorateName)
{
if (expression == null)
throw new ArgumentNullException("expression");
ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName);
return expression(new TypedTableContext<T>(alias)).Render(context);
}
private static void ApplyWhereInternal(Type modelType, Action<string, DynamicColumn.CompareOperator, object> addCondition, Expression expression) private static void ApplyWhereInternal(Type modelType, Action<string, DynamicColumn.CompareOperator, object> addCondition, Expression expression)
{ {
expression = UnwrapConvert(expression); expression = UnwrapConvert(expression);
@@ -11822,6 +12113,23 @@ namespace DynamORM
.FirstOrDefault() .FirstOrDefault()
?? member.Member.Name; ?? member.Member.Name;
} }
private static string ResolveColumn(Type modelType, string memberName, string alias, Func<string, string> 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) private static Expression UnwrapConvert(Expression expression)
{ {
while (expression is UnaryExpression && while (expression is UnaryExpression &&
@@ -15407,6 +15715,389 @@ namespace DynamORM
} }
} }
} }
namespace TypedSql
{
/// <summary>Render context used by typed SQL DSL nodes.</summary>
public interface ITypedSqlRenderContext
{
/// <summary>Resolve mapped column for given model member.</summary>
string ResolveColumn(Type modelType, string memberName, string alias);
/// <summary>Render value as SQL parameter or literal fragment.</summary>
string RenderValue(object value);
/// <summary>Decorate SQL identifier.</summary>
string DecorateName(string name);
}
/// <summary>Entry point for the typed SQL DSL.</summary>
public static class Sql
{
/// <summary>Create parameterized value expression.</summary>
public static TypedSqlExpression<T> Val<T>(T value)
{
return new TypedSqlValueExpression<T>(value);
}
/// <summary>Create parameterized value expression.</summary>
public static TypedSqlExpression<object> Val(object value)
{
return new TypedSqlValueExpression<object>(value);
}
/// <summary>Create raw SQL expression.</summary>
public static TypedSqlExpression<T> Raw<T>(string sql)
{
return new TypedSqlRawExpression<T>(sql);
}
/// <summary>Create generic function call.</summary>
public static TypedSqlExpression<T> Func<T>(string name, params TypedSqlExpression[] arguments)
{
return new TypedSqlFunctionExpression<T>(name, arguments);
}
/// <summary>Create COUNT(*) expression.</summary>
public static TypedSqlExpression<int> Count()
{
return Raw<int>("COUNT(*)");
}
/// <summary>Create COUNT(expr) expression.</summary>
public static TypedSqlExpression<int> Count(TypedSqlExpression expression)
{
return Func<int>("COUNT", expression);
}
/// <summary>Create COALESCE expression.</summary>
public static TypedSqlExpression<T> Coalesce<T>(params TypedSqlExpression[] expressions)
{
return Func<T>("COALESCE", expressions);
}
/// <summary>Create CASE expression builder.</summary>
public static TypedSqlCaseBuilder<T> Case<T>()
{
return new TypedSqlCaseBuilder<T>();
}
}
/// <summary>Builder for CASE expressions.</summary>
/// <typeparam name="T">Result type.</typeparam>
public sealed class TypedSqlCaseBuilder<T>
{
private readonly IList<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>> _cases = new List<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>>();
/// <summary>Add WHEN ... THEN ... clause.</summary>
public TypedSqlCaseBuilder<T> When(TypedSqlPredicate predicate, object value)
{
_cases.Add(new KeyValuePair<TypedSqlPredicate, TypedSqlExpression>(
predicate,
value as TypedSqlExpression ?? Sql.Val(value)));
return this;
}
/// <summary>Finalize CASE expression with ELSE clause.</summary>
public TypedSqlExpression<T> Else(object value)
{
return new TypedSqlCaseExpression<T>(_cases, value as TypedSqlExpression ?? Sql.Val(value));
}
/// <summary>Finalize CASE expression without ELSE clause.</summary>
public TypedSqlExpression<T> End()
{
return new TypedSqlCaseExpression<T>(_cases, null);
}
}
/// <summary>Base selectable SQL fragment for the typed DSL.</summary>
public abstract class TypedSqlSelectable
{
internal abstract string Render(ITypedSqlRenderContext context);
}
/// <summary>Base SQL expression for the typed DSL.</summary>
public abstract class TypedSqlExpression : TypedSqlSelectable
{
/// <summary>Alias this expression in SELECT clause.</summary>
public TypedSqlAliasedExpression As(string alias)
{
return new TypedSqlAliasedExpression(this, alias);
}
/// <summary>Order ascending.</summary>
public TypedSqlOrderExpression Asc()
{
return new TypedSqlOrderExpression(this, true);
}
/// <summary>Order descending.</summary>
public TypedSqlOrderExpression Desc()
{
return new TypedSqlOrderExpression(this, false);
}
/// <summary>Equality predicate.</summary>
public TypedSqlPredicate Eq(object value)
{
return new TypedSqlBinaryPredicate(this, "=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Inequality predicate.</summary>
public TypedSqlPredicate NotEq(object value)
{
return new TypedSqlBinaryPredicate(this, "<>", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Greater-than predicate.</summary>
public TypedSqlPredicate Gt(object value)
{
return new TypedSqlBinaryPredicate(this, ">", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Greater-than-or-equal predicate.</summary>
public TypedSqlPredicate Gte(object value)
{
return new TypedSqlBinaryPredicate(this, ">=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Less-than predicate.</summary>
public TypedSqlPredicate Lt(object value)
{
return new TypedSqlBinaryPredicate(this, "<", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Less-than-or-equal predicate.</summary>
public TypedSqlPredicate Lte(object value)
{
return new TypedSqlBinaryPredicate(this, "<=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>IS NULL predicate.</summary>
public TypedSqlPredicate IsNull()
{
return new TypedSqlUnaryPredicate(this, "IS NULL");
}
/// <summary>IS NOT NULL predicate.</summary>
public TypedSqlPredicate IsNotNull()
{
return new TypedSqlUnaryPredicate(this, "IS NOT NULL");
}
}
/// <summary>Typed SQL expression.</summary>
public abstract class TypedSqlExpression<T> : TypedSqlExpression
{
}
/// <summary>Typed SQL predicate expression.</summary>
public abstract class TypedSqlPredicate : TypedSqlExpression<bool>
{
/// <summary>Combine with AND.</summary>
public TypedSqlPredicate And(TypedSqlPredicate right)
{
return new TypedSqlCombinedPredicate(this, "AND", right);
}
/// <summary>Combine with OR.</summary>
public TypedSqlPredicate Or(TypedSqlPredicate right)
{
return new TypedSqlCombinedPredicate(this, "OR", right);
}
/// <summary>Negate predicate.</summary>
public TypedSqlPredicate Not()
{
return new TypedSqlNegatedPredicate(this);
}
}
/// <summary>Aliased SQL expression.</summary>
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));
}
}
/// <summary>Ordered SQL expression.</summary>
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<T> : TypedSqlExpression<T>
{
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<T> : TypedSqlExpression<T>
{
private readonly object _value;
internal TypedSqlValueExpression(object value)
{
_value = value;
}
internal override string Render(ITypedSqlRenderContext context)
{
return context.RenderValue(_value);
}
}
internal sealed class TypedSqlRawExpression<T> : TypedSqlExpression<T>
{
private readonly string _sql;
internal TypedSqlRawExpression(string sql)
{
_sql = sql;
}
internal override string Render(ITypedSqlRenderContext context)
{
return _sql;
}
}
internal sealed class TypedSqlFunctionExpression<T> : TypedSqlExpression<T>
{
private readonly string _name;
private readonly IList<TypedSqlExpression> _arguments;
internal TypedSqlFunctionExpression(string name, params TypedSqlExpression[] arguments)
{
_name = name;
_arguments = arguments ?? new TypedSqlExpression[0];
}
internal override string Render(ITypedSqlRenderContext context)
{
List<string> rendered = new List<string>();
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<object> && 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<T> : TypedSqlExpression<T>
{
private readonly IList<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>> _cases;
private readonly TypedSqlExpression _elseExpression;
internal TypedSqlCaseExpression(IList<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>> cases, TypedSqlExpression elseExpression)
{
_cases = cases;
_elseExpression = elseExpression;
}
internal override string Render(ITypedSqlRenderContext context)
{
List<string> items = new List<string>();
items.Add("CASE");
foreach (KeyValuePair<TypedSqlPredicate, TypedSqlExpression> 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());
}
}
/// <summary>Typed table context used by the typed SQL DSL.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam>
public sealed class TypedTableContext<T>
{
internal TypedTableContext(string alias)
{
Alias = alias;
}
/// <summary>Gets table alias used by the current query.</summary>
public string Alias { get; private set; }
/// <summary>Creates a mapped column expression.</summary>
public TypedSqlExpression<TValue> Col<TValue>(Expression<Func<T, TValue>> 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<TValue>(typeof(T), member.Member.Name, Alias);
}
}
}
namespace Validation namespace Validation
{ {
/// <summary>Required attribute can be used to validate fields in objects using mapper class.</summary> /// <summary>Required attribute can be used to validate fields in objects using mapper class.</summary>

View File

@@ -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<TypedFluentUser>("u")
.SelectSql(
u => Sql.Count().As("cnt"),
u => Sql.Coalesce<string>(u.Col(x => x.Code), Sql.Val("N/A")).As("code_value"))
.GroupBySql(u => Sql.Coalesce<string>(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<TypedFluentUser>("u")
.SelectSql(u => Sql.Case<string>()
.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<TypedFluentUser>("u")
.Join<TypedFluentUser>(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<Users>()
.SetSql(u => u.Code, u => Sql.Coalesce<string>(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<Users>()
.InsertSql(u => u.Code, u => Sql.Coalesce<string>(Sql.Val("901"), Sql.Val("fallback")))
.InsertSql(u => u.First, u => Sql.Case<string>().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<Users>()
.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()));
}
}
}

View File

@@ -6,6 +6,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.TypedSql;
namespace DynamORM.Builders namespace DynamORM.Builders
{ {
@@ -17,5 +18,8 @@ namespace DynamORM.Builders
/// <param name="predicate">Predicate to parse.</param> /// <param name="predicate">Predicate to parse.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedDeleteQueryBuilder<T> Where(Expression<Func<T, bool>> predicate); IDynamicTypedDeleteQueryBuilder<T> Where(Expression<Func<T, bool>> predicate);
/// <summary>Add typed SQL DSL where predicate.</summary>
IDynamicTypedDeleteQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
} }
} }

View File

@@ -6,6 +6,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.TypedSql;
namespace DynamORM.Builders namespace DynamORM.Builders
{ {
@@ -24,5 +25,8 @@ namespace DynamORM.Builders
/// <param name="value">Mapped object value.</param> /// <param name="value">Mapped object value.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedInsertQueryBuilder<T> Insert(T value); IDynamicTypedInsertQueryBuilder<T> Insert(T value);
/// <summary>Add typed SQL DSL insert assignment.</summary>
IDynamicTypedInsertQueryBuilder<T> InsertSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, TypedSqlExpression> valueFactory);
} }
} }

View File

@@ -28,6 +28,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.TypedSql;
namespace DynamORM.Builders namespace DynamORM.Builders
{ {
@@ -89,5 +90,20 @@ namespace DynamORM.Builders
/// <param name="selectors">Additional selectors to parse.</param> /// <param name="selectors">Additional selectors to parse.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors); IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors);
/// <summary>Add typed SQL DSL select items.</summary>
IDynamicTypedSelectQueryBuilder<T> SelectSql(Func<TypedTableContext<T>, TypedSqlSelectable> selector, params Func<TypedTableContext<T>, TypedSqlSelectable>[] selectors);
/// <summary>Add typed SQL DSL where predicate.</summary>
IDynamicTypedSelectQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
/// <summary>Add typed SQL DSL having predicate.</summary>
IDynamicTypedSelectQueryBuilder<T> HavingSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
/// <summary>Add typed SQL DSL group by expressions.</summary>
IDynamicTypedSelectQueryBuilder<T> GroupBySql(Func<TypedTableContext<T>, TypedSqlExpression> selector, params Func<TypedTableContext<T>, TypedSqlExpression>[] selectors);
/// <summary>Add typed SQL DSL order by expressions.</summary>
IDynamicTypedSelectQueryBuilder<T> OrderBySql(Func<TypedTableContext<T>, TypedSqlOrderExpression> selector, params Func<TypedTableContext<T>, TypedSqlOrderExpression>[] selectors);
} }
} }

View File

@@ -6,6 +6,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.TypedSql;
namespace DynamORM.Builders namespace DynamORM.Builders
{ {
@@ -29,5 +30,11 @@ namespace DynamORM.Builders
/// <param name="value">Mapped object value.</param> /// <param name="value">Mapped object value.</param>
/// <returns>Builder instance.</returns> /// <returns>Builder instance.</returns>
IDynamicTypedUpdateQueryBuilder<T> Values(T value); IDynamicTypedUpdateQueryBuilder<T> Values(T value);
/// <summary>Add typed SQL DSL where predicate.</summary>
IDynamicTypedUpdateQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, TypedSqlPredicate> predicate);
/// <summary>Add typed SQL DSL assignment.</summary>
IDynamicTypedUpdateQueryBuilder<T> SetSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, TypedSqlExpression> valueFactory);
} }
} }

View File

@@ -40,8 +40,8 @@ namespace DynamORM.Builders.Implementation
/// <summary>Implementation of dynamic insert query builder.</summary> /// <summary>Implementation of dynamic insert query builder.</summary>
internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder internal class DynamicInsertQueryBuilder : DynamicModifyBuilder, IDynamicInsertQueryBuilder
{ {
private string _columns; protected string _columns;
private string _values; protected string _values;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DynamicInsertQueryBuilder"/> class. /// Initializes a new instance of the <see cref="DynamicInsertQueryBuilder"/> class.
@@ -221,4 +221,4 @@ namespace DynamORM.Builders.Implementation
#endregion IExtendedDisposable #endregion IExtendedDisposable
} }
} }

View File

@@ -48,11 +48,11 @@ namespace DynamORM.Builders.Implementation
private int? _offset = null; private int? _offset = null;
private bool _distinct = false; private bool _distinct = false;
private string _select; protected string _select;
private string _from; private string _from;
protected string _join; protected string _join;
private string _groupby; protected string _groupby;
private string _orderby; protected string _orderby;
#region IQueryWithHaving #region IQueryWithHaving

View File

@@ -7,6 +7,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.Builders.Extensions; using DynamORM.Builders.Extensions;
using DynamORM.TypedSql;
namespace DynamORM.Builders.Implementation namespace DynamORM.Builders.Implementation
{ {
@@ -34,6 +35,17 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedDeleteQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, 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<T> Where(Func<dynamic, object> func) public new IDynamicTypedDeleteQueryBuilder<T> Where(Func<dynamic, object> func)
{ {
base.Where(func); base.Where(func);
@@ -63,5 +75,14 @@ namespace DynamORM.Builders.Implementation
base.Where(conditions, schema); base.Where(conditions, schema);
return this; return this;
} }
private string RenderValue(object value)
{
if (value == null)
return "NULL";
DynamicSchemaColumn? columnSchema = null;
return ParseConstant(value, Parameters, columnSchema);
}
} }
} }

View File

@@ -7,6 +7,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.Builders.Extensions; using DynamORM.Builders.Extensions;
using DynamORM.TypedSql;
namespace DynamORM.Builders.Implementation namespace DynamORM.Builders.Implementation
{ {
@@ -40,6 +41,16 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedInsertQueryBuilder<T> InsertSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, 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<T> Values(Func<dynamic, object> fn, params Func<dynamic, object>[] func) public new IDynamicTypedInsertQueryBuilder<T> Values(Func<dynamic, object> fn, params Func<dynamic, object>[] func)
{ {
base.Values(fn, func); base.Values(fn, func);
@@ -57,5 +68,14 @@ namespace DynamORM.Builders.Implementation
base.Insert(o); base.Insert(o);
return this; return this;
} }
private string RenderValue(object value)
{
if (value == null)
return "NULL";
DynamicSchemaColumn? columnSchema = null;
return ParseConstant(value, Parameters, columnSchema);
}
} }
} }

View File

@@ -34,6 +34,7 @@ using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using DynamORM.Helpers; using DynamORM.Helpers;
using DynamORM.Mapper; using DynamORM.Mapper;
using DynamORM.TypedSql;
namespace DynamORM.Builders.Implementation namespace DynamORM.Builders.Implementation
{ {
@@ -41,6 +42,45 @@ namespace DynamORM.Builders.Implementation
/// <typeparam name="T">Mapped entity type.</typeparam> /// <typeparam name="T">Mapped entity type.</typeparam>
internal class DynamicTypedSelectQueryBuilder<T> : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder<T> internal class DynamicTypedSelectQueryBuilder<T> : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder<T>
{ {
private sealed class TypedSqlRenderContext : ITypedSqlRenderContext
{
private readonly DynamicTypedSelectQueryBuilder<T> _builder;
public TypedSqlRenderContext(DynamicTypedSelectQueryBuilder<T> 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; private readonly DynamicTypeMap _mapper;
public DynamicTypedSelectQueryBuilder(DynamicDatabase db) public DynamicTypedSelectQueryBuilder(DynamicDatabase db)
@@ -141,7 +181,12 @@ namespace DynamORM.Builders.Implementation
if (SupportNoLock && spec.UseNoLock) if (SupportNoLock && spec.UseNoLock)
joinExpr += " WITH(NOLOCK)"; 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<T>(GetRootAliasOrTableName()), new TypedTableContext<TRight>(rightAlias)).Render(context));
}
else if (!string.IsNullOrEmpty(spec.OnRawCondition))
joinExpr += string.Format(" ON {0}", spec.OnRawCondition); joinExpr += string.Format(" ON {0}", spec.OnRawCondition);
AppendJoinClause(joinExpr); AppendJoinClause(joinExpr);
@@ -197,6 +242,19 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> SelectSql(Func<TypedTableContext<T>, TypedSqlSelectable> selector, params Func<TypedTableContext<T>, TypedSqlSelectable>[] selectors)
{
if (selector == null)
throw new ArgumentNullException("selector");
AddSelectSqlSelector(selector);
if (selectors != null)
foreach (Func<TypedTableContext<T>, TypedSqlSelectable> item in selectors)
AddSelectSqlSelector(item);
return this;
}
public IDynamicTypedSelectQueryBuilder<T> GroupBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors) public IDynamicTypedSelectQueryBuilder<T> GroupBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors)
{ {
if (selector == null) if (selector == null)
@@ -216,6 +274,19 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> GroupBySql(Func<TypedTableContext<T>, TypedSqlExpression> selector, params Func<TypedTableContext<T>, TypedSqlExpression>[] selectors)
{
if (selector == null)
throw new ArgumentNullException("selector");
AddGroupBySqlSelector(selector);
if (selectors != null)
foreach (Func<TypedTableContext<T>, TypedSqlExpression> item in selectors)
AddGroupBySqlSelector(item);
return this;
}
public IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors) public IDynamicTypedSelectQueryBuilder<T> OrderBy<TResult>(Expression<Func<T, TResult>> selector, params Expression<Func<T, TResult>>[] selectors)
{ {
if (selector == null) if (selector == null)
@@ -235,6 +306,19 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> OrderBySql(Func<TypedTableContext<T>, TypedSqlOrderExpression> selector, params Func<TypedTableContext<T>, TypedSqlOrderExpression>[] selectors)
{
if (selector == null)
throw new ArgumentNullException("selector");
AddOrderBySqlSelector(selector);
if (selectors != null)
foreach (Func<TypedTableContext<T>, TypedSqlOrderExpression> item in selectors)
AddOrderBySqlSelector(item);
return this;
}
public new IDynamicTypedSelectQueryBuilder<T> Top(int? top) public new IDynamicTypedSelectQueryBuilder<T> Top(int? top)
{ {
base.Top(top); base.Top(top);
@@ -274,6 +358,34 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedSelectQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, 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<T> HavingSql(Func<TypedTableContext<T>, 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<T> Having(DynamicColumn column) public new IDynamicTypedSelectQueryBuilder<T> Having(DynamicColumn column)
{ {
base.Having(column); base.Having(column);
@@ -317,6 +429,17 @@ namespace DynamORM.Builders.Implementation
} }
} }
private void AddSelectSqlSelector(Func<TypedTableContext<T>, TypedSqlSelectable> selector)
{
if (selector == null)
throw new ArgumentNullException("selector");
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
TypedSqlSelectable item = selector(new TypedTableContext<T>(GetRootAliasOrTableName()));
string rendered = item.Render(context);
_select = string.IsNullOrEmpty(_select) ? rendered : string.Format("{0}, {1}", _select, rendered);
}
private void AddGroupBySelector<TResult>(Expression<Func<T, TResult>> selector) private void AddGroupBySelector<TResult>(Expression<Func<T, TResult>> selector)
{ {
var body = UnwrapConvert(selector.Body); var body = UnwrapConvert(selector.Body);
@@ -336,6 +459,17 @@ namespace DynamORM.Builders.Implementation
} }
} }
private void AddGroupBySqlSelector(Func<TypedTableContext<T>, TypedSqlExpression> selector)
{
if (selector == null)
throw new ArgumentNullException("selector");
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
TypedSqlExpression item = selector(new TypedTableContext<T>(GetRootAliasOrTableName()));
string rendered = item.Render(context);
_groupby = string.IsNullOrEmpty(_groupby) ? rendered : string.Format("{0}, {1}", _groupby, rendered);
}
private void AddOrderBySelector<TResult>(Expression<Func<T, TResult>> selector) private void AddOrderBySelector<TResult>(Expression<Func<T, TResult>> selector)
{ {
var body = UnwrapConvert(selector.Body); var body = UnwrapConvert(selector.Body);
@@ -352,6 +486,23 @@ namespace DynamORM.Builders.Implementation
((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed); ((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed);
} }
private void AddOrderBySqlSelector(Func<TypedTableContext<T>, TypedSqlOrderExpression> selector)
{
if (selector == null)
throw new ArgumentNullException("selector");
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
TypedSqlOrderExpression item = selector(new TypedTableContext<T>(GetRootAliasOrTableName()));
string rendered = item.Render(context);
_orderby = string.IsNullOrEmpty(_orderby) ? rendered : string.Format("{0}, {1}", _orderby, rendered);
}
private string RenderSqlPredicate(Func<TypedTableContext<T>, TypedSqlPredicate> predicate)
{
TypedSqlRenderContext context = new TypedSqlRenderContext(this);
return predicate(new TypedTableContext<T>(GetRootAliasOrTableName())).Render(context);
}
private string ParseTypedCondition(Expression expression) private string ParseTypedCondition(Expression expression)
{ {
expression = UnwrapConvert(expression); expression = UnwrapConvert(expression);

View File

@@ -7,6 +7,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.Builders.Extensions; using DynamORM.Builders.Extensions;
using DynamORM.TypedSql;
namespace DynamORM.Builders.Implementation namespace DynamORM.Builders.Implementation
{ {
@@ -46,6 +47,26 @@ namespace DynamORM.Builders.Implementation
return this; return this;
} }
public IDynamicTypedUpdateQueryBuilder<T> WhereSql(Func<TypedTableContext<T>, 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<T> SetSql<TValue>(Expression<Func<T, TValue>> selector, Func<TypedTableContext<T>, 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<T> Update(string column, object value) public new IDynamicTypedUpdateQueryBuilder<T> Update(string column, object value)
{ {
base.Update(column, value); base.Update(column, value);
@@ -105,5 +126,14 @@ namespace DynamORM.Builders.Implementation
base.Where(conditions, schema); base.Where(conditions, schema);
return this; return this;
} }
private string RenderValue(object value)
{
if (value == null)
return "NULL";
DynamicSchemaColumn? columnSchema = null;
return ParseConstant(value, Parameters, columnSchema);
}
} }
} }

View File

@@ -41,7 +41,7 @@ namespace DynamORM.Builders.Implementation
/// <summary>Update query builder.</summary> /// <summary>Update query builder.</summary>
internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere
{ {
private string _columns; protected string _columns;
internal DynamicUpdateQueryBuilder(DynamicDatabase db) internal DynamicUpdateQueryBuilder(DynamicDatabase db)
: base(db) : base(db)
@@ -339,4 +339,4 @@ namespace DynamORM.Builders.Implementation
#endregion IExtendedDisposable #endregion IExtendedDisposable
} }
} }

View File

@@ -5,15 +5,46 @@
*/ */
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.Mapper; using DynamORM.Mapper;
using DynamORM.TypedSql;
namespace DynamORM.Builders.Implementation namespace DynamORM.Builders.Implementation
{ {
/// <summary>Helper methods for typed modify builders.</summary> /// <summary>Helper methods for typed modify builders.</summary>
internal static class TypedModifyHelper internal static class TypedModifyHelper
{ {
private sealed class ModifyRenderContext : ITypedSqlRenderContext
{
private readonly Func<Type, string, string, Func<string, string>, string> _resolveColumn;
private readonly Func<object, string> _renderValue;
private readonly Func<string, string> _decorateName;
public ModifyRenderContext(Func<Type, string, string, Func<string, string>, string> resolveColumn, Func<object, string> renderValue, Func<string, string> 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<T, TValue>(Expression<Func<T, TValue>> selector) public static string GetMappedColumn<T, TValue>(Expression<Func<T, TValue>> selector)
{ {
if (selector == null) if (selector == null)
@@ -32,6 +63,32 @@ namespace DynamORM.Builders.Implementation
ApplyWhereInternal(typeof(T), addCondition, predicate.Body); ApplyWhereInternal(typeof(T), addCondition, predicate.Body);
} }
public static string RenderPredicate<T>(
Func<TypedTableContext<T>, TypedSqlPredicate> predicate,
string alias,
Func<object, string> renderValue,
Func<string, string> decorateName)
{
if (predicate == null)
throw new ArgumentNullException("predicate");
ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName);
return predicate(new TypedTableContext<T>(alias)).Render(context);
}
public static string RenderExpression<T>(
Func<TypedTableContext<T>, TypedSqlExpression> expression,
string alias,
Func<object, string> renderValue,
Func<string, string> decorateName)
{
if (expression == null)
throw new ArgumentNullException("expression");
ModifyRenderContext context = new ModifyRenderContext(ResolveColumn, renderValue, decorateName);
return expression(new TypedTableContext<T>(alias)).Render(context);
}
private static void ApplyWhereInternal(Type modelType, Action<string, DynamicColumn.CompareOperator, object> addCondition, Expression expression) private static void ApplyWhereInternal(Type modelType, Action<string, DynamicColumn.CompareOperator, object> addCondition, Expression expression)
{ {
expression = UnwrapConvert(expression); expression = UnwrapConvert(expression);
@@ -93,6 +150,25 @@ namespace DynamORM.Builders.Implementation
?? member.Member.Name; ?? member.Member.Name;
} }
private static string ResolveColumn(Type modelType, string memberName, string alias, Func<string, string> 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) private static Expression UnwrapConvert(Expression expression)
{ {
while (expression is UnaryExpression && while (expression is UnaryExpression &&

View File

@@ -6,6 +6,7 @@
using System; using System;
using System.Linq.Expressions; using System.Linq.Expressions;
using DynamORM.TypedSql;
namespace DynamORM.Builders namespace DynamORM.Builders
{ {
@@ -37,6 +38,9 @@ namespace DynamORM.Builders
/// <summary>Gets raw ON condition.</summary> /// <summary>Gets raw ON condition.</summary>
public string OnRawCondition { get; private set; } public string OnRawCondition { get; private set; }
/// <summary>Gets typed SQL DSL ON specification.</summary>
public Func<TypedTableContext<TLeft>, TypedTableContext<TRight>, TypedSqlPredicate> OnSqlPredicate { get; private set; }
/// <summary>Sets join alias.</summary> /// <summary>Sets join alias.</summary>
public TypedJoinBuilder<TLeft, TRight> As(string alias) public TypedJoinBuilder<TLeft, TRight> As(string alias)
{ {
@@ -133,6 +137,7 @@ namespace DynamORM.Builders
OnPredicate = predicate; OnPredicate = predicate;
OnRawCondition = null; OnRawCondition = null;
OnSqlPredicate = null;
return this; return this;
} }
@@ -144,6 +149,19 @@ namespace DynamORM.Builders
OnRawCondition = condition.Trim(); OnRawCondition = condition.Trim();
OnPredicate = null; OnPredicate = null;
OnSqlPredicate = null;
return this;
}
/// <summary>Sets typed SQL DSL ON predicate.</summary>
public TypedJoinBuilder<TLeft, TRight> OnSql(Func<TypedTableContext<TLeft>, TypedTableContext<TRight>, TypedSqlPredicate> predicate)
{
if (predicate == null)
throw new ArgumentNullException("predicate");
OnSqlPredicate = predicate;
OnPredicate = null;
OnRawCondition = null;
return this; return this;
} }
} }

View File

@@ -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
{
/// <summary>Render context used by typed SQL DSL nodes.</summary>
public interface ITypedSqlRenderContext
{
/// <summary>Resolve mapped column for given model member.</summary>
string ResolveColumn(Type modelType, string memberName, string alias);
/// <summary>Render value as SQL parameter or literal fragment.</summary>
string RenderValue(object value);
/// <summary>Decorate SQL identifier.</summary>
string DecorateName(string name);
}
}

91
DynamORM/TypedSql/Sql.cs Normal file
View File

@@ -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
{
/// <summary>Entry point for the typed SQL DSL.</summary>
public static class Sql
{
/// <summary>Create parameterized value expression.</summary>
public static TypedSqlExpression<T> Val<T>(T value)
{
return new TypedSqlValueExpression<T>(value);
}
/// <summary>Create parameterized value expression.</summary>
public static TypedSqlExpression<object> Val(object value)
{
return new TypedSqlValueExpression<object>(value);
}
/// <summary>Create raw SQL expression.</summary>
public static TypedSqlExpression<T> Raw<T>(string sql)
{
return new TypedSqlRawExpression<T>(sql);
}
/// <summary>Create generic function call.</summary>
public static TypedSqlExpression<T> Func<T>(string name, params TypedSqlExpression[] arguments)
{
return new TypedSqlFunctionExpression<T>(name, arguments);
}
/// <summary>Create COUNT(*) expression.</summary>
public static TypedSqlExpression<int> Count()
{
return Raw<int>("COUNT(*)");
}
/// <summary>Create COUNT(expr) expression.</summary>
public static TypedSqlExpression<int> Count(TypedSqlExpression expression)
{
return Func<int>("COUNT", expression);
}
/// <summary>Create COALESCE expression.</summary>
public static TypedSqlExpression<T> Coalesce<T>(params TypedSqlExpression[] expressions)
{
return Func<T>("COALESCE", expressions);
}
/// <summary>Create CASE expression builder.</summary>
public static TypedSqlCaseBuilder<T> Case<T>()
{
return new TypedSqlCaseBuilder<T>();
}
}
/// <summary>Builder for CASE expressions.</summary>
/// <typeparam name="T">Result type.</typeparam>
public sealed class TypedSqlCaseBuilder<T>
{
private readonly IList<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>> _cases = new List<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>>();
/// <summary>Add WHEN ... THEN ... clause.</summary>
public TypedSqlCaseBuilder<T> When(TypedSqlPredicate predicate, object value)
{
_cases.Add(new KeyValuePair<TypedSqlPredicate, TypedSqlExpression>(
predicate,
value as TypedSqlExpression ?? Sql.Val(value)));
return this;
}
/// <summary>Finalize CASE expression with ELSE clause.</summary>
public TypedSqlExpression<T> Else(object value)
{
return new TypedSqlCaseExpression<T>(_cases, value as TypedSqlExpression ?? Sql.Val(value));
}
/// <summary>Finalize CASE expression without ELSE clause.</summary>
public TypedSqlExpression<T> End()
{
return new TypedSqlCaseExpression<T>(_cases, null);
}
}
}

View File

@@ -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
{
/// <summary>Base selectable SQL fragment for the typed DSL.</summary>
public abstract class TypedSqlSelectable
{
internal abstract string Render(ITypedSqlRenderContext context);
}
/// <summary>Base SQL expression for the typed DSL.</summary>
public abstract class TypedSqlExpression : TypedSqlSelectable
{
/// <summary>Alias this expression in SELECT clause.</summary>
public TypedSqlAliasedExpression As(string alias)
{
return new TypedSqlAliasedExpression(this, alias);
}
/// <summary>Order ascending.</summary>
public TypedSqlOrderExpression Asc()
{
return new TypedSqlOrderExpression(this, true);
}
/// <summary>Order descending.</summary>
public TypedSqlOrderExpression Desc()
{
return new TypedSqlOrderExpression(this, false);
}
/// <summary>Equality predicate.</summary>
public TypedSqlPredicate Eq(object value)
{
return new TypedSqlBinaryPredicate(this, "=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Inequality predicate.</summary>
public TypedSqlPredicate NotEq(object value)
{
return new TypedSqlBinaryPredicate(this, "<>", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Greater-than predicate.</summary>
public TypedSqlPredicate Gt(object value)
{
return new TypedSqlBinaryPredicate(this, ">", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Greater-than-or-equal predicate.</summary>
public TypedSqlPredicate Gte(object value)
{
return new TypedSqlBinaryPredicate(this, ">=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Less-than predicate.</summary>
public TypedSqlPredicate Lt(object value)
{
return new TypedSqlBinaryPredicate(this, "<", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>Less-than-or-equal predicate.</summary>
public TypedSqlPredicate Lte(object value)
{
return new TypedSqlBinaryPredicate(this, "<=", value is TypedSqlExpression ? (TypedSqlExpression)value : Sql.Val(value));
}
/// <summary>IS NULL predicate.</summary>
public TypedSqlPredicate IsNull()
{
return new TypedSqlUnaryPredicate(this, "IS NULL");
}
/// <summary>IS NOT NULL predicate.</summary>
public TypedSqlPredicate IsNotNull()
{
return new TypedSqlUnaryPredicate(this, "IS NOT NULL");
}
}
/// <summary>Typed SQL expression.</summary>
public abstract class TypedSqlExpression<T> : TypedSqlExpression
{
}
/// <summary>Typed SQL predicate expression.</summary>
public abstract class TypedSqlPredicate : TypedSqlExpression<bool>
{
/// <summary>Combine with AND.</summary>
public TypedSqlPredicate And(TypedSqlPredicate right)
{
return new TypedSqlCombinedPredicate(this, "AND", right);
}
/// <summary>Combine with OR.</summary>
public TypedSqlPredicate Or(TypedSqlPredicate right)
{
return new TypedSqlCombinedPredicate(this, "OR", right);
}
/// <summary>Negate predicate.</summary>
public TypedSqlPredicate Not()
{
return new TypedSqlNegatedPredicate(this);
}
}
/// <summary>Aliased SQL expression.</summary>
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));
}
}
/// <summary>Ordered SQL expression.</summary>
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<T> : TypedSqlExpression<T>
{
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<T> : TypedSqlExpression<T>
{
private readonly object _value;
internal TypedSqlValueExpression(object value)
{
_value = value;
}
internal override string Render(ITypedSqlRenderContext context)
{
return context.RenderValue(_value);
}
}
internal sealed class TypedSqlRawExpression<T> : TypedSqlExpression<T>
{
private readonly string _sql;
internal TypedSqlRawExpression(string sql)
{
_sql = sql;
}
internal override string Render(ITypedSqlRenderContext context)
{
return _sql;
}
}
internal sealed class TypedSqlFunctionExpression<T> : TypedSqlExpression<T>
{
private readonly string _name;
private readonly IList<TypedSqlExpression> _arguments;
internal TypedSqlFunctionExpression(string name, params TypedSqlExpression[] arguments)
{
_name = name;
_arguments = arguments ?? new TypedSqlExpression[0];
}
internal override string Render(ITypedSqlRenderContext context)
{
List<string> rendered = new List<string>();
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<object> && 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<T> : TypedSqlExpression<T>
{
private readonly IList<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>> _cases;
private readonly TypedSqlExpression _elseExpression;
internal TypedSqlCaseExpression(IList<KeyValuePair<TypedSqlPredicate, TypedSqlExpression>> cases, TypedSqlExpression elseExpression)
{
_cases = cases;
_elseExpression = elseExpression;
}
internal override string Render(ITypedSqlRenderContext context)
{
List<string> items = new List<string>();
items.Add("CASE");
foreach (KeyValuePair<TypedSqlPredicate, TypedSqlExpression> 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());
}
}
}

View File

@@ -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
{
/// <summary>Typed table context used by the typed SQL DSL.</summary>
/// <typeparam name="T">Mapped entity type.</typeparam>
public sealed class TypedTableContext<T>
{
internal TypedTableContext(string alias)
{
Alias = alias;
}
/// <summary>Gets table alias used by the current query.</summary>
public string Alias { get; private set; }
/// <summary>Creates a mapped column expression.</summary>
public TypedSqlExpression<TValue> Col<TValue>(Expression<Func<T, TValue>> 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<TValue>(typeof(T), member.Member.Name, Alias);
}
}
}