From c0fb2e5232436736adeaef1bcecefdee92d7dd09 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 08:49:00 +0100 Subject: [PATCH] Add arithmetic and joined-alias helpers to typed SQL DSL --- AmalgamationTool/DynamORM.Amalgamation.cs | 110 ++++++++++++++++++- DynamORM.Tests/TypedSql/TypedSqlDslTests.cs | 60 ++++++++++ DynamORM/TypedSql/Sql.cs | 12 ++ DynamORM/TypedSql/TypedSqlExpression.cs | 115 +++++++++++++++++++- 4 files changed, 291 insertions(+), 6 deletions(-) diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 4ea5c27..6124e23 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -15830,11 +15830,21 @@ namespace DynamORM { return new TypedSqlValueExpression(value); } + /// Create typed table context for an explicitly named alias, including joined aliases. + public static TypedTableContext Table(string alias) + { + return new TypedTableContext(alias); + } /// Create raw SQL expression. public static TypedSqlExpression Raw(string sql) { return new TypedSqlRawExpression(sql); } + /// Create raw ORDER BY fragment. + public static TypedSqlOrderExpression RawOrder(string sql) + { + return new TypedSqlRawOrderExpression(Raw(sql)); + } /// Create generic function call. public static TypedSqlExpression Func(string name, params TypedSqlExpression[] arguments) { @@ -15979,6 +15989,31 @@ namespace DynamORM /// Base SQL expression for the typed DSL. public abstract class TypedSqlExpression : TypedSqlSelectable { + /// Add arithmetic expression. + public static TypedSqlExpression operator +(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "+", right); + } + /// Subtract arithmetic expression. + public static TypedSqlExpression operator -(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "-", right); + } + /// Multiply arithmetic expression. + public static TypedSqlExpression operator *(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "*", right); + } + /// Divide arithmetic expression. + public static TypedSqlExpression operator /(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "/", right); + } + /// Modulo arithmetic expression. + public static TypedSqlExpression operator %(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "%", right); + } /// Alias this expression in SELECT clause. public TypedSqlAliasedExpression As(string alias) { @@ -15994,6 +16029,31 @@ namespace DynamORM { return new TypedSqlOrderExpression(this, false); } + /// Add expression to another value. + public TypedSqlExpression Add(object value) + { + return new TypedSqlBinaryExpression(this, "+", value as TypedSqlExpression ?? Sql.Val(value)); + } + /// Subtract another value. + public TypedSqlExpression Sub(object value) + { + return new TypedSqlBinaryExpression(this, "-", value as TypedSqlExpression ?? Sql.Val(value)); + } + /// Multiply by another value. + public TypedSqlExpression Mul(object value) + { + return new TypedSqlBinaryExpression(this, "*", value as TypedSqlExpression ?? Sql.Val(value)); + } + /// Divide by another value. + public TypedSqlExpression Div(object value) + { + return new TypedSqlBinaryExpression(this, "/", value as TypedSqlExpression ?? Sql.Val(value)); + } + /// Modulo by another value. + public TypedSqlExpression Mod(object value) + { + return new TypedSqlBinaryExpression(this, "%", value as TypedSqlExpression ?? Sql.Val(value)); + } /// Equality predicate. public TypedSqlPredicate Eq(object value) { @@ -16120,19 +16180,63 @@ namespace DynamORM } } /// Ordered SQL expression. - public sealed class TypedSqlOrderExpression : TypedSqlSelectable + public class TypedSqlOrderExpression : TypedSqlSelectable { private readonly TypedSqlExpression _expression; private readonly bool _ascending; + private readonly string _nullOrdering; + private readonly bool _raw; - internal TypedSqlOrderExpression(TypedSqlExpression expression, bool ascending) + internal TypedSqlOrderExpression(TypedSqlExpression expression, bool ascending, string nullOrdering = null, bool raw = false) { _expression = expression; _ascending = ascending; + _nullOrdering = nullOrdering; + _raw = raw; + } + /// Append NULLS FIRST ordering. + public TypedSqlOrderExpression NullsFirst() + { + return new TypedSqlOrderExpression(_expression, _ascending, "NULLS FIRST", _raw); + } + /// Append NULLS LAST ordering. + public TypedSqlOrderExpression NullsLast() + { + return new TypedSqlOrderExpression(_expression, _ascending, "NULLS LAST", _raw); } internal override string Render(ITypedSqlRenderContext context) { - return string.Format("{0} {1}", _expression.Render(context), _ascending ? "ASC" : "DESC"); + string rendered = _raw + ? _expression.Render(context) + : string.Format("{0} {1}", _expression.Render(context), _ascending ? "ASC" : "DESC"); + if (!string.IsNullOrEmpty(_nullOrdering)) + rendered = string.Format("{0} {1}", rendered, _nullOrdering); + + return rendered; + } + } + internal sealed class TypedSqlRawOrderExpression : TypedSqlOrderExpression + { + internal TypedSqlRawOrderExpression(TypedSqlExpression expression) + : base(expression, true, null, true) + { + } + } + internal sealed class TypedSqlBinaryExpression : TypedSqlExpression + { + private readonly TypedSqlExpression _left; + private readonly string _operator; + private readonly TypedSqlExpression _right; + + internal TypedSqlBinaryExpression(TypedSqlExpression left, string op, TypedSqlExpression 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 TypedSqlColumnExpression : TypedSqlExpression diff --git a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs index 54e7dc1..225e783 100644 --- a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs +++ b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs @@ -202,6 +202,22 @@ namespace DynamORM.Tests.TypedSql NormalizeSql(cmd.CommandText())); } + [Test] + public void TestSelectSqlSupportsArithmeticExpressions() + { + var cmd = Database.FromTyped("u") + .SelectSql( + u => (u.Col(x => x.Id) + Sql.Val(1)).As("plus_one"), + u => u.Col(x => x.Id).Sub(2).As("minus_two"), + u => u.Col(x => x.Id).Mul(3).As("times_three"), + u => u.Col(x => x.Id).Div(4).As("div_four"), + u => u.Col(x => x.Id).Mod(5).As("mod_five")); + + Assert.AreEqual( + "SELECT (u.\"id_user\" + [$0]) AS \"plus_one\", (u.\"id_user\" - [$1]) AS \"minus_two\", (u.\"id_user\" * [$2]) AS \"times_three\", (u.\"id_user\" / [$3]) AS \"div_four\", (u.\"id_user\" % [$4]) AS \"mod_five\" FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + [Test] public void TestSelectSqlSupportsCustomFunction() { @@ -224,6 +240,36 @@ namespace DynamORM.Tests.TypedSql NormalizeSql(cmd.CommandText())); } + [Test] + public void TestSelectSqlSupportsJoinedAliasHelpers() + { + var other = Sql.Table("x"); + 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)))) + .SelectSql( + u => u.All(), + u => other.All(), + u => other.Col(x => x.Code).As("joined_code")); + + Assert.AreEqual( + "SELECT u.*, x.*, x.\"user_code\" AS \"joined_code\" FROM \"sample_users\" AS u LEFT JOIN \"sample_users\" AS x ON (u.\"id_user\" = x.\"id_user\")", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestWhereSqlSupportsJoinedAliasHelpers() + { + var other = Sql.Table("x"); + 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)))) + .WhereSql(u => other.Col(x => x.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\") WHERE (x.\"user_code\" IS NOT NULL)", + NormalizeSql(cmd.CommandText())); + } + [Test] public void TestSelectSqlSupportsScalarSubQuery() { @@ -271,6 +317,20 @@ namespace DynamORM.Tests.TypedSql NormalizeSql(cmd.CommandText())); } + [Test] + public void TestOrderBySqlSupportsNullOrderingAndRawFragments() + { + var cmd = Database.FromTyped("u") + .SelectSql(u => u.Col(x => x.Id)) + .OrderBySql( + u => u.Col(x => x.Code).Asc().NullsLast(), + u => Sql.RawOrder("2 DESC")); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u ORDER BY u.\"user_code\" ASC NULLS LAST, 2 DESC", + NormalizeSql(cmd.CommandText())); + } + [Test] public void TestWhereSqlSupportsTypedExistsHelper() { diff --git a/DynamORM/TypedSql/Sql.cs b/DynamORM/TypedSql/Sql.cs index 265f96e..eea24df 100644 --- a/DynamORM/TypedSql/Sql.cs +++ b/DynamORM/TypedSql/Sql.cs @@ -25,12 +25,24 @@ namespace DynamORM.TypedSql return new TypedSqlValueExpression(value); } + /// Create typed table context for an explicitly named alias, including joined aliases. + public static TypedTableContext Table(string alias) + { + return new TypedTableContext(alias); + } + /// Create raw SQL expression. public static TypedSqlExpression Raw(string sql) { return new TypedSqlRawExpression(sql); } + /// Create raw ORDER BY fragment. + public static TypedSqlOrderExpression RawOrder(string sql) + { + return new TypedSqlRawOrderExpression(Raw(sql)); + } + /// Create generic function call. public static TypedSqlExpression Func(string name, params TypedSqlExpression[] arguments) { diff --git a/DynamORM/TypedSql/TypedSqlExpression.cs b/DynamORM/TypedSql/TypedSqlExpression.cs index ab08813..a13c7d0 100644 --- a/DynamORM/TypedSql/TypedSqlExpression.cs +++ b/DynamORM/TypedSql/TypedSqlExpression.cs @@ -20,6 +20,36 @@ namespace DynamORM.TypedSql /// Base SQL expression for the typed DSL. public abstract class TypedSqlExpression : TypedSqlSelectable { + /// Add arithmetic expression. + public static TypedSqlExpression operator +(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "+", right); + } + + /// Subtract arithmetic expression. + public static TypedSqlExpression operator -(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "-", right); + } + + /// Multiply arithmetic expression. + public static TypedSqlExpression operator *(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "*", right); + } + + /// Divide arithmetic expression. + public static TypedSqlExpression operator /(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "/", right); + } + + /// Modulo arithmetic expression. + public static TypedSqlExpression operator %(TypedSqlExpression left, TypedSqlExpression right) + { + return new TypedSqlBinaryExpression(left, "%", right); + } + /// Alias this expression in SELECT clause. public TypedSqlAliasedExpression As(string alias) { @@ -38,6 +68,36 @@ namespace DynamORM.TypedSql return new TypedSqlOrderExpression(this, false); } + /// Add expression to another value. + public TypedSqlExpression Add(object value) + { + return new TypedSqlBinaryExpression(this, "+", value as TypedSqlExpression ?? Sql.Val(value)); + } + + /// Subtract another value. + public TypedSqlExpression Sub(object value) + { + return new TypedSqlBinaryExpression(this, "-", value as TypedSqlExpression ?? Sql.Val(value)); + } + + /// Multiply by another value. + public TypedSqlExpression Mul(object value) + { + return new TypedSqlBinaryExpression(this, "*", value as TypedSqlExpression ?? Sql.Val(value)); + } + + /// Divide by another value. + public TypedSqlExpression Div(object value) + { + return new TypedSqlBinaryExpression(this, "/", value as TypedSqlExpression ?? Sql.Val(value)); + } + + /// Modulo by another value. + public TypedSqlExpression Mod(object value) + { + return new TypedSqlBinaryExpression(this, "%", value as TypedSqlExpression ?? Sql.Val(value)); + } + /// Equality predicate. public TypedSqlPredicate Eq(object value) { @@ -187,20 +247,69 @@ namespace DynamORM.TypedSql } /// Ordered SQL expression. - public sealed class TypedSqlOrderExpression : TypedSqlSelectable + public class TypedSqlOrderExpression : TypedSqlSelectable { private readonly TypedSqlExpression _expression; private readonly bool _ascending; + private readonly string _nullOrdering; + private readonly bool _raw; - internal TypedSqlOrderExpression(TypedSqlExpression expression, bool ascending) + internal TypedSqlOrderExpression(TypedSqlExpression expression, bool ascending, string nullOrdering = null, bool raw = false) { _expression = expression; _ascending = ascending; + _nullOrdering = nullOrdering; + _raw = raw; + } + + /// Append NULLS FIRST ordering. + public TypedSqlOrderExpression NullsFirst() + { + return new TypedSqlOrderExpression(_expression, _ascending, "NULLS FIRST", _raw); + } + + /// Append NULLS LAST ordering. + public TypedSqlOrderExpression NullsLast() + { + return new TypedSqlOrderExpression(_expression, _ascending, "NULLS LAST", _raw); } internal override string Render(ITypedSqlRenderContext context) { - return string.Format("{0} {1}", _expression.Render(context), _ascending ? "ASC" : "DESC"); + string rendered = _raw + ? _expression.Render(context) + : string.Format("{0} {1}", _expression.Render(context), _ascending ? "ASC" : "DESC"); + if (!string.IsNullOrEmpty(_nullOrdering)) + rendered = string.Format("{0} {1}", rendered, _nullOrdering); + + return rendered; + } + } + + internal sealed class TypedSqlRawOrderExpression : TypedSqlOrderExpression + { + internal TypedSqlRawOrderExpression(TypedSqlExpression expression) + : base(expression, true, null, true) + { + } + } + + internal sealed class TypedSqlBinaryExpression : TypedSqlExpression + { + private readonly TypedSqlExpression _left; + private readonly string _operator; + private readonly TypedSqlExpression _right; + + internal TypedSqlBinaryExpression(TypedSqlExpression left, string op, TypedSqlExpression 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)); } }