Skip to content

Commit

Permalink
Merge pull request #538 from mk3008/537-enhancements-in-function-any-…
Browse files Browse the repository at this point in the history
…function

Enhancement: Added In and Any functions
  • Loading branch information
mk3008 authored Sep 6, 2024
2 parents 3a91af1 + c868e01 commit 50ac664
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 0 deletions.
38 changes: 38 additions & 0 deletions src/Carbunql/Fluent/SelectQueryParameterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

public static class SelectQueryParameterExtensions
{
/// <summary>
/// Adds a parameter to the query. This method is obsolete and will be removed in a future version.
/// Please use the <see cref="Parameter"/> method instead.
/// </summary>
/// <param name="query">The query to which the parameter will be added.</param>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The value of the parameter.</param>
/// <returns>The modified query with the parameter added.</returns>
[Obsolete("AddParameter is obsolete. Please use the 'Parameter' method instead.")]
public static SelectQuery AddParameter(this SelectQuery query, string name, object? value)
{
Expand All @@ -10,17 +18,47 @@ public static SelectQuery AddParameter(this SelectQuery query, string name, obje
return query;
}

/// <summary>
/// Adds a parameter to the query.
/// </summary>
/// <param name="query">The query to which the parameter will be added.</param>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The value of the parameter.</param>
/// <returns>The modified query with the parameter added.</returns>
public static SelectQuery Parameter(this SelectQuery query, string name, object? value)
{
var prm = new QueryParameter(name, value);
query.Parameters.Add(prm);
return query;
}

/// <summary>
/// Adds a parameter to the query and applies a function that uses the parameter's name.
/// </summary>
/// <param name="query">The query to which the parameter will be added.</param>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The value of the parameter.</param>
/// <param name="func">A function that takes the parameter's name and returns a modified query.</param>
/// <returns>The result of applying the function to the parameter's name.</returns>
public static SelectQuery Parameter(this SelectQuery query, string name, object? value, Func<string, SelectQuery> func)
{
var prm = new QueryParameter(name, value);
query.Parameters.Add(prm);
return func(prm.ParameterName);
}

/// <summary>
/// Adds a parameter to the query and applies a function that uses both the query and the parameter's name.
/// </summary>
/// <param name="query">The query to which the parameter will be added.</param>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The value of the parameter.</param>
/// <param name="func">A function that takes the query and the parameter's name, and returns a modified query.</param>
/// <returns>The result of applying the function to the query and the parameter's name.</returns>
public static SelectQuery Parameter(this SelectQuery query, string name, object? value, Func<SelectQuery, string, SelectQuery> func)
{
var prm = new QueryParameter(name, value);
query.Parameters.Add(prm);
return func(query, prm.ParameterName);
}
}
117 changes: 117 additions & 0 deletions src/Carbunql/Fluent/SelectQueryWhereExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Carbunql.Building;
using Carbunql.Fluent;
using Cysharp.Text;

namespace Carbunql.Fluent;

Expand Down Expand Up @@ -49,6 +50,20 @@ public static SelectQuery IfNotNullOrEmpty(this SelectQuery query, string? value
: query;
}

/// <summary>
/// Applies the specified function to the query if the provided collection contains any elements.
/// </summary>
/// <param name="query">The query to which the function will be applied.</param>
/// <param name="value">The collection to check for any elements.</param>
/// <param name="func">The function to apply to the query if the collection contains any elements.</param>
/// <returns>The modified query if the collection contains any elements; otherwise, returns the original query.</returns>
public static SelectQuery IfNotEmpty<T>(this SelectQuery query, IEnumerable<T> value, Func<SelectQuery, SelectQuery> func)
{
return value.Any()
? func(query)
: query;
}

private static (string, string) GenerateComparison(string operatorSymbol, object? value)
{
if (value == null)
Expand Down Expand Up @@ -525,4 +540,106 @@ public static SelectQuery HasDifferent(this SelectQuery query, FluentTable left,
{
return query.HasDifferent(left.Alias, right.Alias, validationColumns);
}

/// <summary>
/// Adds an `IN` condition to the query where the specified column value matches any value in the given collection.
/// </summary>
/// <typeparam name="T">The type of the values in the collection. Must be a non-nullable type.</typeparam>
/// <param name="query">The query to which the `IN` condition will be added.</param>
/// <param name="columnName">The name of the column to check against the values.</param>
/// <param name="values">The collection of values to match against. Values are converted to strings and added to the condition.</param>
/// <returns>The modified query with the `IN` condition applied.</returns>
/// <exception cref="InvalidOperationException">Thrown if the type of any string value is not parameterized properly.</exception>
public static SelectQuery In<T>(this SelectQuery query, string columnName, IEnumerable<T> 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;
}

/// <summary>
/// Adds an `IN` condition to the query where the specified column value matches any value in the results of the provided subquery.
/// </summary>
/// <param name="query">The query to which the `IN` condition will be added.</param>
/// <param name="columnName">The name of the column to check against the subquery results.</param>
/// <param name="selectQuery">The subquery whose results will be used for the `IN` condition.</param>
/// <returns>The modified query with the `IN` condition applied.</returns>
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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="query">The query to which the `IN` condition will be added.</param>
/// <param name="tableName">The name of the table to apply the condition to.</param>
/// <param name="columnName">The name of the column to check against the subquery results.</param>
/// <param name="selectQuery">The subquery whose results will be used for the `IN` condition.</param>
/// <returns>The modified query with the `IN` condition applied.</returns>
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;
}

/// <summary>
/// Applies an `ANY` condition to the query, checking if the specified column value matches any value in the array parameter.
/// </summary>
/// <param name="query">The query to which the condition will be applied.</param>
/// <param name="columnName">The name of the column to check against the array values.</param>
/// <param name="arrayParameter">The parameter representing the array of values to match against. This should be a parameterized placeholder.</param>
/// <returns>The modified query with the `ANY` condition applied.</returns>
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;
}
}
136 changes: 136 additions & 0 deletions test/Carbunql.Building.Test/QuerySourceFilterTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Carbunql.Fluent;
using System.Diagnostics;
using Xunit.Abstractions;

namespace Carbunql.Building.Test;
Expand Down Expand Up @@ -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);
}
}

0 comments on commit 50ac664

Please sign in to comment.