From c868e01c1f72ead692bb54e31c4eb091628c60d4 Mon Sep 17 00:00:00 2001 From: mk3008 Date: Fri, 6 Sep 2024 22:30:58 +0900 Subject: [PATCH] Enhancement: Added In and Any functions The Any function only works with Postgres. --- .../Fluent/SelectQueryParameterExtensions.cs | 38 +++++ .../Fluent/SelectQueryWhereExtensions.cs | 117 +++++++++++++++ .../QuerySourceFilterTest.cs | 136 ++++++++++++++++++ 3 files changed, 291 insertions(+) diff --git a/src/Carbunql/Fluent/SelectQueryParameterExtensions.cs b/src/Carbunql/Fluent/SelectQueryParameterExtensions.cs index 1536fa12..0693b30b 100644 --- a/src/Carbunql/Fluent/SelectQueryParameterExtensions.cs +++ b/src/Carbunql/Fluent/SelectQueryParameterExtensions.cs @@ -2,6 +2,14 @@ public static class SelectQueryParameterExtensions { + /// + /// Adds a parameter to the query. This method is obsolete and will be removed in a future version. + /// Please use the method instead. + /// + /// The query to which the parameter will be added. + /// The name of the parameter. + /// The value of the parameter. + /// The modified query with the parameter added. [Obsolete("AddParameter is obsolete. Please use the 'Parameter' method instead.")] public static SelectQuery AddParameter(this SelectQuery query, string name, object? value) { @@ -10,6 +18,13 @@ public static SelectQuery AddParameter(this SelectQuery query, string name, obje return query; } + /// + /// Adds a parameter to the query. + /// + /// The query to which the parameter will be added. + /// The name of the parameter. + /// The value of the parameter. + /// The modified query with the parameter added. public static SelectQuery Parameter(this SelectQuery query, string name, object? value) { var prm = new QueryParameter(name, value); @@ -17,10 +32,33 @@ public static SelectQuery Parameter(this SelectQuery query, string name, object? return query; } + /// + /// Adds a parameter to the query and applies a function that uses the parameter's name. + /// + /// The query to which the parameter will be added. + /// The name of the parameter. + /// The value of the parameter. + /// A function that takes the parameter's name and returns a modified query. + /// The result of applying the function to the parameter's name. public static SelectQuery Parameter(this SelectQuery query, string name, object? value, Func func) { var prm = new QueryParameter(name, value); query.Parameters.Add(prm); return func(prm.ParameterName); } + + /// + /// Adds a parameter to the query and applies a function that uses both the query and the parameter's name. + /// + /// The query to which the parameter will be added. + /// The name of the parameter. + /// The value of the parameter. + /// A function that takes the query and the parameter's name, and returns a modified query. + /// The result of applying the function to the query and the parameter's name. + public static SelectQuery Parameter(this SelectQuery query, string name, object? value, Func func) + { + var prm = new QueryParameter(name, value); + query.Parameters.Add(prm); + return func(query, prm.ParameterName); + } } diff --git a/src/Carbunql/Fluent/SelectQueryWhereExtensions.cs b/src/Carbunql/Fluent/SelectQueryWhereExtensions.cs index d408d4b6..0d8691b3 100644 --- a/src/Carbunql/Fluent/SelectQueryWhereExtensions.cs +++ b/src/Carbunql/Fluent/SelectQueryWhereExtensions.cs @@ -1,5 +1,6 @@ using Carbunql.Building; using Carbunql.Fluent; +using Cysharp.Text; namespace Carbunql.Fluent; @@ -49,6 +50,20 @@ public static SelectQuery IfNotNullOrEmpty(this SelectQuery query, string? value : query; } + /// + /// Applies the specified function to the query if the provided collection contains any elements. + /// + /// The query to which the function will be applied. + /// The collection to check for any elements. + /// The function to apply to the query if the collection contains any elements. + /// The modified query if the collection contains any elements; otherwise, returns the original query. + public static SelectQuery IfNotEmpty(this SelectQuery query, IEnumerable value, Func func) + { + return value.Any() + ? func(query) + : query; + } + private static (string, string) GenerateComparison(string operatorSymbol, object? value) { if (value == null) @@ -525,4 +540,106 @@ public static SelectQuery HasDifferent(this SelectQuery query, FluentTable left, { return query.HasDifferent(left.Alias, right.Alias, validationColumns); } + + /// + /// Adds an `IN` condition to the query where the specified column value matches any value in the given collection. + /// + /// The type of the values in the collection. Must be a non-nullable type. + /// The query to which the `IN` condition will be added. + /// The name of the column to check against the values. + /// The collection of values to match against. Values are converted to strings and added to the condition. + /// The modified query with the `IN` condition applied. + /// Thrown if the type of any string value is not parameterized properly. + public static SelectQuery In(this SelectQuery query, string columnName, IEnumerable values) where T : notnull + { + var sb = ZString.CreateStringBuilder(); + foreach (var item in values) + { + // Check if the type is string + if (typeof(T) == typeof(string)) + { + var stringItem = item as string; + if (stringItem != null && !ParameterSymbols.Any(stringItem.StartsWith)) + { + throw new InvalidOperationException("The string type must be parameterized."); + } + } + + if (sb.Length != 0) sb.Append(", "); + + if (typeof(T) == typeof(DateTime) && item is DateTime dateItem) + { + sb.Append($"'{dateItem.ToString()}'"); + } + else + { + sb.Append(item); + } + } + + query.AddWhere(columnName, (source, column) => $"{column} in ({sb.ToString()})", true); + + return query; + } + + /// + /// Adds an `IN` condition to the query where the specified column value matches any value in the results of the provided subquery. + /// + /// The query to which the `IN` condition will be added. + /// The name of the column to check against the subquery results. + /// The subquery whose results will be used for the `IN` condition. + /// The modified query with the `IN` condition applied. + public static SelectQuery In(this SelectQuery query, string columnName, SelectQuery selectQuery) + { + // Import parameters from the subquery + selectQuery.GetParameters().ForEach(p => + { + query.AddParameter(p.ParameterName, p.Value); + }); + + query.AddWhere(columnName, (source, column) => $"{column} in ({selectQuery.ToOneLineText()})", true); + + return query; + } + + /// + /// Adds an `IN` condition to the query where the specified column value matches any value in the results of the provided subquery. + /// This version allows specifying the table name for more complex queries. + /// + /// The query to which the `IN` condition will be added. + /// The name of the table to apply the condition to. + /// The name of the column to check against the subquery results. + /// The subquery whose results will be used for the `IN` condition. + /// The modified query with the `IN` condition applied. + public static SelectQuery In(this SelectQuery query, string tableName, string columnName, SelectQuery selectQuery) + { + // Import parameters from the subquery + selectQuery.GetParameters().ForEach(p => + { + query.AddParameter(p.ParameterName, p.Value); + }); + + query.AddWhere(tableName, columnName, (source, column) => $"{column} in ({selectQuery.ToOneLineText()})"); + + return query; + } + + /// + /// Applies an `ANY` condition to the query, checking if the specified column value matches any value in the array parameter. + /// + /// The query to which the condition will be applied. + /// The name of the column to check against the array values. + /// The parameter representing the array of values to match against. This should be a parameterized placeholder. + /// The modified query with the `ANY` condition applied. + public static SelectQuery Any(this SelectQuery query, string columnName, string arrayParameter) + { + if (!ParameterSymbols.Any(arrayParameter.StartsWith)) + { + throw new InvalidOperationException("The string type must be parameterized."); + } + + query.AddWhere(columnName, (source, column) => $"{column} = any ({arrayParameter})", true); + + return query; + } } diff --git a/test/Carbunql.Building.Test/QuerySourceFilterTest.cs b/test/Carbunql.Building.Test/QuerySourceFilterTest.cs index b66f9d33..53237ea0 100644 --- a/test/Carbunql.Building.Test/QuerySourceFilterTest.cs +++ b/test/Carbunql.Building.Test/QuerySourceFilterTest.cs @@ -1,4 +1,5 @@ using Carbunql.Fluent; +using System.Diagnostics; using Xunit.Abstractions; namespace Carbunql.Building.Test; @@ -1422,4 +1423,139 @@ sale AS s Assert.Equal(expect, query.ToText(), true, true, true); } + + [Fact] + public void InTest() + { + var sql = """ + select + s.unit_price * s.amount as price + from + sale as s + """; + + int[] prices = [10, 20, 30]; + + var query = SelectQuery.Parse(sql) + .In("price", prices); + + Monitor.Log(query, exportTokens: false); + + var expect = """ + SELECT + s.unit_price * s.amount AS price + FROM + sale AS s + WHERE + s.unit_price * s.amount IN (10, 20, 30) + """; + + Assert.Equal(expect, query.ToText(), true, true, true); + } + + [Fact] + public void IfNotEmpty() + { + var sql = """ + select + s.unit_price * s.amount as price + from + sale as s + """; + + int[] prices = []; + + var query = SelectQuery.Parse(sql) + .IfNotEmpty(prices, x => x.In("price", prices)); + + Monitor.Log(query, exportTokens: false); + + var expect = """ + SELECT + s.unit_price * s.amount AS price + FROM + sale AS s + """; + + Assert.Equal(expect, query.ToText(), true, true, true); + + prices = [10, 20, 30]; + + query = SelectQuery.Parse(sql) + .IfNotEmpty(prices, x => x.In("price", prices)); + + Monitor.Log(query, exportTokens: false); + + expect = """ + SELECT + s.unit_price * s.amount AS price + FROM + sale AS s + WHERE + s.unit_price * s.amount IN (10, 20, 30) + """; + + Assert.Equal(expect, query.ToText(), true, true, true); + } + + [Fact] + public void InSelectQueryTest() + { + var sql = """ + select + s.unit_price * s.amount as price + from + sale as s + """; + + var query = SelectQuery.Parse(sql) + .In("price", SelectQuery.Parse("select x.value from table_x as x")); + + Monitor.Log(query, exportTokens: false); + + var expect = """ + SELECT + s.unit_price * s.amount AS price + FROM + sale AS s + WHERE + s.unit_price * s.amount IN ( + SELECT + x.value + FROM + table_x AS x + ) + """; + + Assert.Equal(expect, query.ToText(), true, true, true); + } + + [Fact] + public void AnyTest_PostgresOnly() + { + var sql = """ + select + s.unit_price * s.amount as price + from + sale as s + """; + + int[] prices = [10, 20, 30]; + + var query = SelectQuery.Parse(sql) + .Parameter(":prices", prices, (q, parameter) => q.Any("price", parameter)); + + Monitor.Log(query, exportTokens: false); + + var expect = """ + SELECT + s.unit_price * s.amount AS price + FROM + sale AS s + WHERE + s.unit_price * s.amount = ANY(:prices) + """; + + Assert.Equal(expect, query.ToText(), true, true, true); + } }