Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 71 additions & 11 deletions src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,28 @@ public class CosmosQuerySqlGenerator(ITypeMappingSource typeMappingSource) : Sql
{
private readonly IndentedStringBuilder _sqlBuilder = new();
private IReadOnlyDictionary<string, object> _parameterValues = null!;

/// <summary>
/// The Cosmos SqlParameters which will get sent in the CosmosQuery.
/// </summary>
private List<SqlParameter> _sqlParameters = null!;

/// <summary>
/// Lookup table from <see cref="SqlParameterExpression.Name" /> (the "original" name) to the Cosmos <see cref="SqlParameter" />,
/// whose name has underdone uniquification and prefixing. This is used when the same parameter is referenced multiple times in the query.
/// </summary>
private Dictionary<string, SqlParameter> _sqlParametersByOriginalName = null!;

/// <summary>
/// Contains final parameter names (prefixed, uniquified) seen so far, for uniquification purposes.
/// </summary>
private readonly HashSet<string> _prefixedParameterNames = new(StringComparer.OrdinalIgnoreCase);

private ParameterNameGenerator _parameterNameGenerator = null!;

private static readonly bool UseOldBehavior37252 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37252", out var enabled) && enabled;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -32,6 +51,7 @@ public virtual CosmosSqlQuery GetSqlQuery(
_sqlBuilder.Clear();
_parameterValues = parameterValues;
_sqlParameters = [];
_sqlParametersByOriginalName = [];
_parameterNameGenerator = new ParameterNameGenerator();

Visit(selectExpression);
Expand Down Expand Up @@ -401,7 +421,11 @@ when _parameterValues.TryGetValue(queryParameter.Name, out var parameterValue)
substitutions = new string[parameterValues.Length];
for (var i = 0; i < parameterValues.Length; i++)
{
var parameterName = _parameterNameGenerator.GenerateNext();
// Note that we don't go through _sqlParametersByOriginalName, since the FromSql parameters we're adding here cannot
// be referenced multiple times.
var parameterName = UseOldBehavior37252
? _parameterNameGenerator.GenerateNext()
: PrefixAndUniquifyParameterName("p");
_sqlParameters.Add(new SqlParameter(parameterName, parameterValues[i]));
substitutions[i] = parameterName;
}
Expand Down Expand Up @@ -677,20 +701,42 @@ protected override Expression VisitSqlConditional(SqlConditionalExpression sqlCo
/// </summary>
protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression)
{
var parameterName = $"@{sqlParameterExpression.Name}";

if (_sqlParameters.All(sp => sp.Name != parameterName))
if (UseOldBehavior37252)
{
Check.DebugAssert(sqlParameterExpression.TypeMapping is not null, "SqlParameterExpression without a type mapping.");
var parameterName = $"@{sqlParameterExpression.Name}";

if (_sqlParameters.All(sp => sp.Name != parameterName))
{
Check.DebugAssert(sqlParameterExpression.TypeMapping is not null, "SqlParameterExpression without a type mapping.");

_sqlParameters.Add(
new SqlParameter(
parameterName,
((CosmosTypeMapping)sqlParameterExpression.TypeMapping)
.GenerateJToken(_parameterValues[sqlParameterExpression.Name])));
}

_sqlParameters.Add(
new SqlParameter(
parameterName,
((CosmosTypeMapping)sqlParameterExpression.TypeMapping)
.GenerateJToken(_parameterValues[sqlParameterExpression.Name])));
_sqlBuilder.Append(parameterName);
}
else
{
if (!_sqlParametersByOriginalName.TryGetValue(sqlParameterExpression.Name, out var sqlParameter))
{
Check.DebugAssert(sqlParameterExpression.TypeMapping is not null, "SqlParameterExpression without a type mapping.");

var parameterName = PrefixAndUniquifyParameterName(sqlParameterExpression.Name);

_sqlBuilder.Append(parameterName);
sqlParameter = new SqlParameter(
parameterName,
((CosmosTypeMapping)sqlParameterExpression.TypeMapping)
.GenerateJToken(_parameterValues[sqlParameterExpression.Name]));

_sqlParametersByOriginalName[sqlParameterExpression.Name] = sqlParameter;
_sqlParameters.Add(sqlParameter);
}

_sqlBuilder.Append(sqlParameter.Name);
}

return sqlParameterExpression;
}
Expand Down Expand Up @@ -863,6 +909,20 @@ private void GenerateFunction(string name, IReadOnlyList<Expression> arguments)
_sqlBuilder.Append(')');
}

private string PrefixAndUniquifyParameterName(string originalName)
{
var i = 0;
var prefixedName = $"@{originalName}";

while (_prefixedParameterNames.Contains(prefixedName))
{
prefixedName = $"@{originalName + i++}";
}

_prefixedParameterNames.Add(prefixedName);
return prefixedName;
}

private sealed class ParameterNameGenerator
{
private int _count;
Expand Down
81 changes: 59 additions & 22 deletions test/EFCore.Cosmos.FunctionalTests/Query/FromSqlQueryCosmosTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,12 @@ public Task FromSqlRaw_queryable_with_parameters(bool async)

AssertSql(
"""
@p0='London'
@p1='Sales Representative'
@p='London'
@p0='Sales Representative'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p0 AND c["ContactTitle"] = @p1
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p AND c["ContactTitle"] = @p0
) s
""");
});
Expand All @@ -388,12 +388,12 @@ public Task FromSqlRaw_queryable_with_parameters_inline(bool async)

AssertSql(
"""
@p0='London'
@p1='Sales Representative'
@p='London'
@p0='Sales Representative'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p0 AND c["ContactTitle"] = @p1
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p AND c["ContactTitle"] = @p0
) s
""");
});
Expand All @@ -418,11 +418,11 @@ public Task FromSqlRaw_queryable_with_null_parameter(bool async)

AssertSql(
"""
@p0=null
@p=null

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["$type"] = "Employee" AND c["ReportsTo"] = @p0 OR (IS_NULL(c["ReportsTo"]) AND IS_NULL(@p0))
SELECT * FROM root c WHERE c["$type"] = "Employee" AND c["ReportsTo"] = @p OR (IS_NULL(c["ReportsTo"]) AND IS_NULL(@p))
) s
""");
});
Expand Down Expand Up @@ -451,12 +451,12 @@ public Task FromSqlRaw_queryable_with_parameters_and_closure(bool async)

AssertSql(
"""
@p0='London'
@p='London'
@contactTitle='Sales Representative'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p0
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p
) s
WHERE (s["ContactTitle"] = @contactTitle)
""");
Expand Down Expand Up @@ -540,22 +540,22 @@ public virtual Task FromSqlRaw_queryable_with_parameters_cache_key_includes_para

AssertSql(
"""
@p0='London'
@p1='Sales Representative'
@p='London'
@p0='Sales Representative'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p0 AND c["ContactTitle"] = @p1
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p AND c["ContactTitle"] = @p0
) s
""",
//
"""
@p0='Madrid'
@p1='Accounting Manager'
@p='Madrid'
@p0='Accounting Manager'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p0 AND c["ContactTitle"] = @p1
SELECT * FROM root c WHERE c["$type"] = "Customer" AND c["City"] = @p AND c["ContactTitle"] = @p0
) s
""");
});
Expand Down Expand Up @@ -731,12 +731,12 @@ await AssertQuery(

AssertSql(
"""
@p0='London'
@p1='Sales Representative'
@p='London'
@p0='Sales Representative'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["City"] = @p0 AND c["ContactTitle"] = @p1
SELECT * FROM root c WHERE c["City"] = @p AND c["ContactTitle"] = @p0
) s
""");
});
Expand All @@ -754,13 +754,50 @@ await AssertQuery(

AssertSql(
"""
@p0='London'
@p1='Sales Representative'
@p='London'
@p0='Sales Representative'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["City"] = @p0 AND c["ContactTitle"] = @p1
SELECT * FROM root c WHERE c["City"] = @p AND c["ContactTitle"] = @p0
) s
""");
});

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual Task Both_FromSql_and_regular_parameters(bool async)
=> Fixture.NoSyncTest(
async, async a =>
{
var city = "London";
var country = "UK";

await AssertQuery(
a,
ss => ((DbSet<Customer>)ss.Set<Customer>())
.FromSql($"""SELECT * FROM root c WHERE c["City"] = {city} AND c["Country"] = {country}""")
.OrderBy(c => c.CustomerID)
.Skip(1) // Non-lambda parameter, so becomes a SqlParameter
.Take(5),
ss => ss.Set<Customer>()
.Where(x => x.City == city)
.OrderBy(c => c.CustomerID)
.Skip(1)
.Take(5));

AssertSql(
"""
@p='London'
@p0='UK'
@p00='1'
@p1='5'

SELECT VALUE s
FROM (
SELECT * FROM root c WHERE c["City"] = @p AND c["Country"] = @p0
) s
ORDER BY s["id"]
OFFSET @p00 LIMIT @p1
""");
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1555,11 +1555,11 @@ public override Task Two_parameters_with_same_case_insensitive_name_get_uniquifi
AssertSql(
"""
@customerID='ANATR'
@customerId='ALFKI'
@customerId0='ALFKI'

SELECT VALUE c
FROM root c
WHERE ((c["id"] = @customerID) OR (c["id"] = @customerId))
WHERE ((c["id"] = @customerID) OR (c["id"] = @customerId0))
""");
});

Expand Down