src/Core/Resolvers/CosmosQueryBuilder.cs (189 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Models;
namespace Azure.DataApiBuilder.Core.Resolvers
{
public class CosmosQueryBuilder : BaseSqlQueryBuilder
{
private readonly string _containerAlias = "c";
/// <summary>
/// Builds a cosmos sql query string
/// </summary>
/// <param name="structure"></param>
/// <returns></returns>
public string Build(CosmosQueryStructure structure)
{
StringBuilder queryStringBuilder = new();
queryStringBuilder.Append($"SELECT {WrappedColumns(structure)}"
+ $" FROM {_containerAlias}");
string predicateString = Build(structure.Predicates);
structure.DbPolicyPredicatesForOperations.TryGetValue(EntityActionOperation.Read, out string? policy);
// If there is a predicate or policy, add a WHERE clause
if (!string.IsNullOrEmpty(predicateString) || !string.IsNullOrEmpty(policy))
{
queryStringBuilder
.Append(" WHERE ")
.Append(string.IsNullOrEmpty(predicateString) || string.IsNullOrEmpty(policy)
? predicateString + policy
: string.Join(" AND ", predicateString, policy));
}
if (structure.OrderByColumns.Count > 0)
{
queryStringBuilder.Append($" ORDER BY {Build(structure.OrderByColumns)}");
}
return queryStringBuilder.ToString();
}
protected override string Build(Column column)
{
string alias = _containerAlias;
if (column.TableAlias != null)
{
alias = column.TableAlias;
}
return alias + "." + column.ColumnName;
}
protected override string Build(KeysetPaginationPredicate? predicate)
{
// Cosmos doesnt do keyset pagination
return string.Empty;
}
public override string QuoteIdentifier(string ident)
{
return ident;
}
/// <summary>
/// Build columns and wrap columns
/// </summary>
private string WrappedColumns(CosmosQueryStructure structure)
{
return string.Join(
", ",
structure.Columns
.Select(c => _containerAlias + "." + c.Label)
.Distinct()
);
}
/// <summary>
/// Resolves a predicate operation enum to string
/// </summary>
protected override string Build(PredicateOperation op)
{
switch (op)
{
case PredicateOperation.Equal:
return "=";
case PredicateOperation.GreaterThan:
return ">";
case PredicateOperation.LessThan:
return "<";
case PredicateOperation.GreaterThanOrEqual:
return ">=";
case PredicateOperation.LessThanOrEqual:
return "<=";
case PredicateOperation.NotEqual:
return "!=";
case PredicateOperation.AND:
return "AND";
case PredicateOperation.OR:
return "OR";
case PredicateOperation.LIKE:
return "LIKE";
case PredicateOperation.NOT_LIKE:
return "NOT LIKE";
case PredicateOperation.IS:
return "";
case PredicateOperation.IS_NOT:
return "NOT";
case PredicateOperation.EXISTS:
return "EXISTS";
case PredicateOperation.ARRAY_CONTAINS:
return "ARRAY_CONTAINS";
case PredicateOperation.NOT_ARRAY_CONTAINS:
return "NOT ARRAY_CONTAINS";
default:
throw new ArgumentException($"Cannot build unknown predicate operation {op}.");
}
}
/// <summary>
/// Build left and right predicate operand and resolve the predicate operator into
/// {OperandLeft} {Operator} {OperandRight}
/// </summary>
protected override string Build(Predicate? predicate)
{
if (predicate is null)
{
throw new ArgumentNullException(nameof(predicate));
}
string predicateString;
if (predicate.Left is not null)
{
if (predicate.Op == PredicateOperation.ARRAY_CONTAINS || predicate.Op == PredicateOperation.NOT_ARRAY_CONTAINS)
{
predicateString = $" {Build(predicate.Op)} ( {ResolveOperand(predicate.Left)}, {ResolveOperand(predicate.Right)})";
}
else if (ResolveOperand(predicate.Right).Equals(GQLFilterParser.NullStringValue))
{
// For Binary predicates:
predicateString = $" {Build(predicate.Op)} IS_NULL({ResolveOperand(predicate.Left)})";
}
else
{
predicateString = $"{ResolveOperand(predicate.Left)} {Build(predicate.Op)} {ResolveOperand(predicate.Right)} ";
}
}
else
{
// For Unary predicates, there is always a parenthesis around the operand.
predicateString = $"{Build(predicate.Op)} ({ResolveOperand(predicate.Right)})";
}
if (predicate.AddParenthesis)
{
return "(" + predicateString + ")";
}
else
{
return predicateString;
}
}
/// <summary>
/// Resolves the operand either as a column, another predicate,
/// a SqlQueryStructure or returns it directly as string
/// </summary>
protected new string ResolveOperand(PredicateOperand? operand)
{
if (operand == null)
{
throw new ArgumentNullException(nameof(operand));
}
Column? column;
string? stringType;
Predicate? predicate;
BaseQueryStructure? sqlQueryStructure;
if ((column = operand.AsColumn()) != null)
{
return Build(column);
}
else if ((stringType = operand.AsString()) != null)
{
return stringType;
}
else if ((predicate = operand.AsPredicate()) != null)
{
return Build(predicate);
}
else if ((sqlQueryStructure = operand.AsCosmosQueryStructure()) is not null
&& sqlQueryStructure is CosmosExistsQueryStructure cosmosExistsQueryStructure)
{
return Build(cosmosExistsQueryStructure);
}
else if ((sqlQueryStructure = operand.AsCosmosQueryStructure()) is not null
&& sqlQueryStructure is CosmosQueryStructure cosmosQueryStructure)
{
return Build(cosmosQueryStructure);
}
else
{
throw new ArgumentException("Cannot get a value from PredicateOperand to build.");
}
}
/// <inheritdoc />
public virtual string Build(CosmosExistsQueryStructure structure)
{
string query = $"SELECT 1 " +
$"FROM {QuoteIdentifier(structure.SourceAlias)} IN {QuoteIdentifier(structure.DatabaseObject.SchemaName)} " +
$"WHERE {Build(structure.Predicates)}";
return query;
}
/// <summary>
/// Generate Cosmos DB Query for the given fromClause and predicates.
/// </summary>
/// <param name="fromClause">Use to generate FROM part in sql along with table and JOINS</param>
/// <param name="predicates">Query Conditions</param>
/// <returns>CosmosDB Exist Query</returns>
public static string BuildExistsQueryForCosmos(string? fromClause, string? predicates)
{
string? existQuery = $"EXISTS " +
$"(SELECT VALUE 1 " +
$"FROM {fromClause} ";
if (!string.IsNullOrEmpty(predicates))
{
existQuery += $"WHERE {predicates})";
}
else
{
existQuery += ")";
}
return existQuery;
}
}
}