Extend typed SQL DSL with pattern helpers and typed subqueries

This commit is contained in:
root
2026-02-27 08:36:47 +01:00
parent 97ab4c1e15
commit f293bd95c6
5 changed files with 200 additions and 6 deletions

View File

@@ -15915,11 +15915,31 @@ namespace DynamORM
{ {
return new TypedSqlSubQueryExpression<T>(query); return new TypedSqlSubQueryExpression<T>(query);
} }
/// <summary>Create scalar typed subquery expression without manually constructing the builder.</summary>
public static TypedSqlExpression<TResult> SubQuery<TModel, TResult>(DynamicDatabase db, Func<IDynamicTypedSelectQueryBuilder<TModel>, 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<TResult>(configure(db.FromTyped<TModel>(alias, noLock)));
}
/// <summary>Create EXISTS predicate.</summary> /// <summary>Create EXISTS predicate.</summary>
public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query) public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query)
{ {
return new TypedSqlExistsPredicate(query); return new TypedSqlExistsPredicate(query);
} }
/// <summary>Create EXISTS predicate from typed subquery factory.</summary>
public static TypedSqlPredicate Exists<TModel>(DynamicDatabase db, Func<IDynamicTypedSelectQueryBuilder<TModel>, 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<TModel>(alias, noLock)));
}
/// <summary>Create CASE expression builder.</summary> /// <summary>Create CASE expression builder.</summary>
public static TypedSqlCaseBuilder<T> Case<T>() public static TypedSqlCaseBuilder<T> Case<T>()
{ {
@@ -16029,11 +16049,36 @@ namespace DynamORM
{ {
return new TypedSqlInPredicate(this, values); return new TypedSqlInPredicate(this, values);
} }
/// <summary>NOT IN predicate.</summary>
public TypedSqlPredicate NotIn(params object[] values)
{
return new TypedSqlInPredicate(this, values, true);
}
/// <summary>NOT IN predicate.</summary>
public TypedSqlPredicate NotIn(IEnumerable values)
{
return new TypedSqlInPredicate(this, values, true);
}
/// <summary>BETWEEN predicate.</summary> /// <summary>BETWEEN predicate.</summary>
public TypedSqlPredicate Between(object lower, object upper) 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)); return new TypedSqlBetweenPredicate(this, lower is TypedSqlExpression ? (TypedSqlExpression)lower : Sql.Val(lower), upper is TypedSqlExpression ? (TypedSqlExpression)upper : Sql.Val(upper));
} }
/// <summary>Starts-with LIKE predicate.</summary>
public TypedSqlPredicate StartsWith(string value)
{
return Like((value ?? string.Empty) + "%");
}
/// <summary>Ends-with LIKE predicate.</summary>
public TypedSqlPredicate EndsWith(string value)
{
return Like("%" + (value ?? string.Empty));
}
/// <summary>Contains LIKE predicate.</summary>
public TypedSqlPredicate Contains(string value)
{
return Like("%" + (value ?? string.Empty) + "%");
}
} }
/// <summary>Typed SQL expression.</summary> /// <summary>Typed SQL expression.</summary>
public abstract class TypedSqlExpression<T> : TypedSqlExpression public abstract class TypedSqlExpression<T> : TypedSqlExpression
@@ -16201,11 +16246,13 @@ namespace DynamORM
{ {
private readonly TypedSqlExpression _left; private readonly TypedSqlExpression _left;
private readonly IEnumerable _values; 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; _left = left;
_values = values; _values = values;
_negated = negated;
} }
internal override string Render(ITypedSqlRenderContext context) internal override string Render(ITypedSqlRenderContext context)
{ {
@@ -16214,9 +16261,9 @@ namespace DynamORM
rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context)); rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context));
if (rendered.Count == 0) 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 internal sealed class TypedSqlBetweenPredicate : TypedSqlPredicate
@@ -16343,6 +16390,11 @@ namespace DynamORM
return new TypedSqlColumnExpression<TValue>(typeof(T), member.Member.Name, Alias); return new TypedSqlColumnExpression<TValue>(typeof(T), member.Member.Name, Alias);
} }
/// <summary>Select all columns from this typed table context.</summary>
public TypedSqlExpression<object> All()
{
return Sql.Raw<object>(string.IsNullOrEmpty(Alias) ? "*" : Alias + ".*");
}
} }
} }
namespace Validation namespace Validation

View File

@@ -130,6 +130,44 @@ namespace DynamORM.Tests.TypedSql
NormalizeSql(cmd.CommandText())); NormalizeSql(cmd.CommandText()));
} }
[Test]
public void TestWhereSqlSupportsNotIn()
{
var cmd = Database.FromTyped<TypedFluentUser>("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<TypedFluentUser>("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<TypedFluentUser>("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] [Test]
public void TestWhereSqlSupportsEmptyIn() public void TestWhereSqlSupportsEmptyIn()
{ {
@@ -175,6 +213,17 @@ namespace DynamORM.Tests.TypedSql
NormalizeSql(cmd.CommandText())); NormalizeSql(cmd.CommandText()));
} }
[Test]
public void TestSelectSqlSupportsWildcardAll()
{
var cmd = Database.FromTyped<TypedFluentUser>("u")
.SelectSql(u => u.All());
Assert.AreEqual(
"SELECT u.* FROM \"sample_users\" AS u",
NormalizeSql(cmd.CommandText()));
}
[Test] [Test]
public void TestSelectSqlSupportsScalarSubQuery() public void TestSelectSqlSupportsScalarSubQuery()
{ {
@@ -206,6 +255,39 @@ namespace DynamORM.Tests.TypedSql
NormalizeSql(cmd.CommandText())); NormalizeSql(cmd.CommandText()));
} }
[Test]
public void TestSelectSqlSupportsTypedSubQueryHelper()
{
var cmd = Database.FromTyped<TypedFluentUser>("u")
.SelectSql(u => Sql.SubQuery<TypedFluentUser, long>(
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<TypedFluentUser>("u")
.WhereSql(u => Sql.Exists<TypedFluentUser>(
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] [Test]
public void TestInsertSqlObjectProjection() public void TestInsertSqlObjectProjection()
{ {

View File

@@ -127,12 +127,34 @@ namespace DynamORM.TypedSql
return new TypedSqlSubQueryExpression<T>(query); return new TypedSqlSubQueryExpression<T>(query);
} }
/// <summary>Create scalar typed subquery expression without manually constructing the builder.</summary>
public static TypedSqlExpression<TResult> SubQuery<TModel, TResult>(DynamicDatabase db, Func<IDynamicTypedSelectQueryBuilder<TModel>, 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<TResult>(configure(db.FromTyped<TModel>(alias, noLock)));
}
/// <summary>Create EXISTS predicate.</summary> /// <summary>Create EXISTS predicate.</summary>
public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query) public static TypedSqlPredicate Exists(IDynamicSelectQueryBuilder query)
{ {
return new TypedSqlExistsPredicate(query); return new TypedSqlExistsPredicate(query);
} }
/// <summary>Create EXISTS predicate from typed subquery factory.</summary>
public static TypedSqlPredicate Exists<TModel>(DynamicDatabase db, Func<IDynamicTypedSelectQueryBuilder<TModel>, 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<TModel>(alias, noLock)));
}
/// <summary>Create CASE expression builder.</summary> /// <summary>Create CASE expression builder.</summary>
public static TypedSqlCaseBuilder<T> Case<T>() public static TypedSqlCaseBuilder<T> Case<T>()
{ {

View File

@@ -104,11 +104,41 @@ namespace DynamORM.TypedSql
return new TypedSqlInPredicate(this, values); return new TypedSqlInPredicate(this, values);
} }
/// <summary>NOT IN predicate.</summary>
public TypedSqlPredicate NotIn(params object[] values)
{
return new TypedSqlInPredicate(this, values, true);
}
/// <summary>NOT IN predicate.</summary>
public TypedSqlPredicate NotIn(IEnumerable values)
{
return new TypedSqlInPredicate(this, values, true);
}
/// <summary>BETWEEN predicate.</summary> /// <summary>BETWEEN predicate.</summary>
public TypedSqlPredicate Between(object lower, object upper) 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)); return new TypedSqlBetweenPredicate(this, lower is TypedSqlExpression ? (TypedSqlExpression)lower : Sql.Val(lower), upper is TypedSqlExpression ? (TypedSqlExpression)upper : Sql.Val(upper));
} }
/// <summary>Starts-with LIKE predicate.</summary>
public TypedSqlPredicate StartsWith(string value)
{
return Like((value ?? string.Empty) + "%");
}
/// <summary>Ends-with LIKE predicate.</summary>
public TypedSqlPredicate EndsWith(string value)
{
return Like("%" + (value ?? string.Empty));
}
/// <summary>Contains LIKE predicate.</summary>
public TypedSqlPredicate Contains(string value)
{
return Like("%" + (value ?? string.Empty) + "%");
}
} }
/// <summary>Typed SQL expression.</summary> /// <summary>Typed SQL expression.</summary>
@@ -299,11 +329,13 @@ namespace DynamORM.TypedSql
{ {
private readonly TypedSqlExpression _left; private readonly TypedSqlExpression _left;
private readonly IEnumerable _values; 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; _left = left;
_values = values; _values = values;
_negated = negated;
} }
internal override string Render(ITypedSqlRenderContext context) internal override string Render(ITypedSqlRenderContext context)
@@ -313,9 +345,9 @@ namespace DynamORM.TypedSql
rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context)); rendered.Add((value as TypedSqlExpression ?? Sql.Val(value)).Render(context));
if (rendered.Count == 0) 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()));
} }
} }

View File

@@ -36,5 +36,11 @@ namespace DynamORM.TypedSql
return new TypedSqlColumnExpression<TValue>(typeof(T), member.Member.Name, Alias); return new TypedSqlColumnExpression<TValue>(typeof(T), member.Member.Name, Alias);
} }
/// <summary>Select all columns from this typed table context.</summary>
public TypedSqlExpression<object> All()
{
return Sql.Raw<object>(string.IsNullOrEmpty(Alias) ? "*" : Alias + ".*");
}
} }
} }