From f293bd95c685750b9e76106a2cece841d5140678 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 08:36:47 +0100 Subject: [PATCH] Extend typed SQL DSL with pattern helpers and typed subqueries --- AmalgamationTool/DynamORM.Amalgamation.cs | 58 ++++++++++++++- DynamORM.Tests/TypedSql/TypedSqlDslTests.cs | 82 +++++++++++++++++++++ DynamORM/TypedSql/Sql.cs | 22 ++++++ DynamORM/TypedSql/TypedSqlExpression.cs | 38 +++++++++- DynamORM/TypedSql/TypedTableContext.cs | 6 ++ 5 files changed, 200 insertions(+), 6 deletions(-) diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 1109c46..4ea5c27 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -15915,11 +15915,31 @@ namespace DynamORM { return new TypedSqlSubQueryExpression(query); } + /// Create scalar typed subquery expression without manually constructing the builder. + public static TypedSqlExpression SubQuery(DynamicDatabase db, Func, IDynamicSelectQueryBuilder> configure, string alias = null, bool noLock = false) + { + if (db == null) + throw new ArgumentNullException("db"); + if (configure == null) + throw new ArgumentNullException("configure"); + + return new TypedSqlSubQueryExpression(configure(db.FromTyped(alias, noLock))); + } /// Create EXISTS predicate. public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query) { return new TypedSqlExistsPredicate(query); } + /// Create EXISTS predicate from typed subquery factory. + public static TypedSqlPredicate Exists(DynamicDatabase db, Func, IDynamicSelectQueryBuilder> configure, string alias = null, bool noLock = false) + { + if (db == null) + throw new ArgumentNullException("db"); + if (configure == null) + throw new ArgumentNullException("configure"); + + return new TypedSqlExistsPredicate(configure(db.FromTyped(alias, noLock))); + } /// Create CASE expression builder. public static TypedSqlCaseBuilder Case() { @@ -16029,11 +16049,36 @@ namespace DynamORM { return new TypedSqlInPredicate(this, values); } + /// NOT IN predicate. + public TypedSqlPredicate NotIn(params object[] values) + { + return new TypedSqlInPredicate(this, values, true); + } + /// NOT IN predicate. + public TypedSqlPredicate NotIn(IEnumerable values) + { + return new TypedSqlInPredicate(this, values, true); + } /// BETWEEN predicate. public TypedSqlPredicate Between(object lower, object upper) { return new TypedSqlBetweenPredicate(this, lower is TypedSqlExpression ? (TypedSqlExpression)lower : Sql.Val(lower), upper is TypedSqlExpression ? (TypedSqlExpression)upper : Sql.Val(upper)); } + /// Starts-with LIKE predicate. + public TypedSqlPredicate StartsWith(string value) + { + return Like((value ?? string.Empty) + "%"); + } + /// Ends-with LIKE predicate. + public TypedSqlPredicate EndsWith(string value) + { + return Like("%" + (value ?? string.Empty)); + } + /// Contains LIKE predicate. + public TypedSqlPredicate Contains(string value) + { + return Like("%" + (value ?? string.Empty) + "%"); + } } /// Typed SQL expression. public abstract class TypedSqlExpression : TypedSqlExpression @@ -16201,11 +16246,13 @@ namespace DynamORM { private readonly TypedSqlExpression _left; private readonly IEnumerable _values; + private readonly bool _negated; - internal TypedSqlInPredicate(TypedSqlExpression left, IEnumerable values) + internal TypedSqlInPredicate(TypedSqlExpression left, IEnumerable values, bool negated = false) { _left = left; _values = values; + _negated = negated; } internal override string Render(ITypedSqlRenderContext context) { @@ -16214,9 +16261,9 @@ namespace DynamORM rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context)); if (rendered.Count == 0) - return "(1 = 0)"; + return _negated ? "(1 = 1)" : "(1 = 0)"; - return string.Format("({0} IN({1}))", _left.Render(context), string.Join(", ", rendered.ToArray())); + return string.Format("({0} {1}({2}))", _left.Render(context), _negated ? "NOT IN" : "IN", string.Join(", ", rendered.ToArray())); } } internal sealed class TypedSqlBetweenPredicate : TypedSqlPredicate @@ -16343,6 +16390,11 @@ namespace DynamORM return new TypedSqlColumnExpression(typeof(T), member.Member.Name, Alias); } + /// Select all columns from this typed table context. + public TypedSqlExpression All() + { + return Sql.Raw(string.IsNullOrEmpty(Alias) ? "*" : Alias + ".*"); + } } } namespace Validation diff --git a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs index 7b2e49c..54e7dc1 100644 --- a/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs +++ b/DynamORM.Tests/TypedSql/TypedSqlDslTests.cs @@ -130,6 +130,44 @@ namespace DynamORM.Tests.TypedSql NormalizeSql(cmd.CommandText())); } + [Test] + public void TestWhereSqlSupportsNotIn() + { + var cmd = Database.FromTyped("u") + .WhereSql(u => u.Col(x => x.Id).NotIn(1, 2)) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (u.\"id_user\" NOT IN([$0], [$1]))", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestWhereSqlSupportsEmptyNotIn() + { + var cmd = Database.FromTyped("u") + .WhereSql(u => u.Col(x => x.Id).NotIn(new int[0])) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (1 = 1)", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestWhereSqlSupportsPatternHelpers() + { + var cmd = Database.FromTyped("u") + .WhereSql(u => u.Col(x => x.Code).StartsWith("AB") + .And(u.Col(x => x.Code).EndsWith("YZ")) + .And(u.Col(x => x.Code).Contains("MID"))) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (((u.\"user_code\" LIKE [$0]) AND (u.\"user_code\" LIKE [$1])) AND (u.\"user_code\" LIKE [$2]))", + NormalizeSql(cmd.CommandText())); + } + [Test] public void TestWhereSqlSupportsEmptyIn() { @@ -175,6 +213,17 @@ namespace DynamORM.Tests.TypedSql NormalizeSql(cmd.CommandText())); } + [Test] + public void TestSelectSqlSupportsWildcardAll() + { + var cmd = Database.FromTyped("u") + .SelectSql(u => u.All()); + + Assert.AreEqual( + "SELECT u.* FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + [Test] public void TestSelectSqlSupportsScalarSubQuery() { @@ -206,6 +255,39 @@ namespace DynamORM.Tests.TypedSql NormalizeSql(cmd.CommandText())); } + [Test] + public void TestSelectSqlSupportsTypedSubQueryHelper() + { + var cmd = Database.FromTyped("u") + .SelectSql(u => Sql.SubQuery( + Database, + sq => sq + .SelectSql(x => x.Col(a => a.Id)) + .WhereSql(x => x.Col(a => a.Code).Eq("A")), + "x").As("sub_id")); + + Assert.AreEqual( + "SELECT (SELECT x.\"id_user\" FROM \"sample_users\" AS x WHERE (x.\"user_code\" = [$0])) AS \"sub_id\" FROM \"sample_users\" AS u", + NormalizeSql(cmd.CommandText())); + } + + [Test] + public void TestWhereSqlSupportsTypedExistsHelper() + { + var cmd = Database.FromTyped("u") + .WhereSql(u => Sql.Exists( + Database, + sq => sq + .SelectSql(x => x.Col(a => a.Id)) + .WhereSql(x => x.Col(a => a.Code).Eq("A")), + "x")) + .SelectSql(u => u.Col(x => x.Id)); + + Assert.AreEqual( + "SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (EXISTS (SELECT x.\"id_user\" FROM \"sample_users\" AS x WHERE (x.\"user_code\" = [$0])))", + NormalizeSql(cmd.CommandText())); + } + [Test] public void TestInsertSqlObjectProjection() { diff --git a/DynamORM/TypedSql/Sql.cs b/DynamORM/TypedSql/Sql.cs index 757eb6b..265f96e 100644 --- a/DynamORM/TypedSql/Sql.cs +++ b/DynamORM/TypedSql/Sql.cs @@ -127,12 +127,34 @@ namespace DynamORM.TypedSql return new TypedSqlSubQueryExpression(query); } + /// Create scalar typed subquery expression without manually constructing the builder. + public static TypedSqlExpression SubQuery(DynamicDatabase db, Func, IDynamicSelectQueryBuilder> configure, string alias = null, bool noLock = false) + { + if (db == null) + throw new ArgumentNullException("db"); + if (configure == null) + throw new ArgumentNullException("configure"); + + return new TypedSqlSubQueryExpression(configure(db.FromTyped(alias, noLock))); + } + /// Create EXISTS predicate. public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query) { return new TypedSqlExistsPredicate(query); } + /// Create EXISTS predicate from typed subquery factory. + public static TypedSqlPredicate Exists(DynamicDatabase db, Func, IDynamicSelectQueryBuilder> configure, string alias = null, bool noLock = false) + { + if (db == null) + throw new ArgumentNullException("db"); + if (configure == null) + throw new ArgumentNullException("configure"); + + return new TypedSqlExistsPredicate(configure(db.FromTyped(alias, noLock))); + } + /// Create CASE expression builder. public static TypedSqlCaseBuilder Case() { diff --git a/DynamORM/TypedSql/TypedSqlExpression.cs b/DynamORM/TypedSql/TypedSqlExpression.cs index 1bdf68e..ab08813 100644 --- a/DynamORM/TypedSql/TypedSqlExpression.cs +++ b/DynamORM/TypedSql/TypedSqlExpression.cs @@ -104,11 +104,41 @@ namespace DynamORM.TypedSql return new TypedSqlInPredicate(this, values); } + /// NOT IN predicate. + public TypedSqlPredicate NotIn(params object[] values) + { + return new TypedSqlInPredicate(this, values, true); + } + + /// NOT IN predicate. + public TypedSqlPredicate NotIn(IEnumerable values) + { + return new TypedSqlInPredicate(this, values, true); + } + /// BETWEEN predicate. public TypedSqlPredicate Between(object lower, object upper) { return new TypedSqlBetweenPredicate(this, lower is TypedSqlExpression ? (TypedSqlExpression)lower : Sql.Val(lower), upper is TypedSqlExpression ? (TypedSqlExpression)upper : Sql.Val(upper)); } + + /// Starts-with LIKE predicate. + public TypedSqlPredicate StartsWith(string value) + { + return Like((value ?? string.Empty) + "%"); + } + + /// Ends-with LIKE predicate. + public TypedSqlPredicate EndsWith(string value) + { + return Like("%" + (value ?? string.Empty)); + } + + /// Contains LIKE predicate. + public TypedSqlPredicate Contains(string value) + { + return Like("%" + (value ?? string.Empty) + "%"); + } } /// Typed SQL expression. @@ -299,11 +329,13 @@ namespace DynamORM.TypedSql { private readonly TypedSqlExpression _left; private readonly IEnumerable _values; + private readonly bool _negated; - internal TypedSqlInPredicate(TypedSqlExpression left, IEnumerable values) + internal TypedSqlInPredicate(TypedSqlExpression left, IEnumerable values, bool negated = false) { _left = left; _values = values; + _negated = negated; } internal override string Render(ITypedSqlRenderContext context) @@ -313,9 +345,9 @@ namespace DynamORM.TypedSql rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context)); if (rendered.Count == 0) - return "(1 = 0)"; + return _negated ? "(1 = 1)" : "(1 = 0)"; - return string.Format("({0} IN({1}))", _left.Render(context), string.Join(", ", rendered.ToArray())); + return string.Format("({0} {1}({2}))", _left.Render(context), _negated ? "NOT IN" : "IN", string.Join(", ", rendered.ToArray())); } } diff --git a/DynamORM/TypedSql/TypedTableContext.cs b/DynamORM/TypedSql/TypedTableContext.cs index 39adad9..4240141 100644 --- a/DynamORM/TypedSql/TypedTableContext.cs +++ b/DynamORM/TypedSql/TypedTableContext.cs @@ -36,5 +36,11 @@ namespace DynamORM.TypedSql return new TypedSqlColumnExpression(typeof(T), member.Member.Name, Alias); } + + /// Select all columns from this typed table context. + public TypedSqlExpression All() + { + return Sql.Raw(string.IsNullOrEmpty(Alias) ? "*" : Alias + ".*"); + } } }