Skip to content
Open
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
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0 AS build
WORKDIR /src
COPY [".", "./"]
RUN dotnet build "./src/Service/Azure.DataApiBuilder.Service.csproj" -c Docker -o /out -r linux-x64
RUN mkdir /package \
&& cp /src/src/Service/dab-config*.json /package/ \
&& find /src/src/Service/ -maxdepth 1 -name "*.gql" -exec cp {} /package/ \;

FROM mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0 AS runtime

COPY --from=build /out /App
# Add default dab-config.json to /App in the image
COPY --from=build /out/dab-config.json /App/dab-config.json
COPY --from=build /package /App
WORKDIR /App
ENV ASPNETCORE_URLS=http://+:5000
ENTRYPOINT ["dotnet", "Azure.DataApiBuilder.Service.dll"]
71 changes: 71 additions & 0 deletions src/Core/Models/GraphQLFilterParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,77 @@ public static Predicate Parse(
op = isNull ? PredicateOperation.IS : PredicateOperation.IS_NOT;
value = GQLFilterParser.NullStringValue;
break;
case "some":
op = PredicateOperation.ARRAY_SOME;

if (value is not List<ObjectFieldNode> elementFields || elementFields.Count == 0)
{
throw new DataApiBuilderException(
message: "Bad syntax: 'some' list filter requires at least one nested filter field.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}

{
IInputValueDefinition elementFilterSchema = argumentObject.Fields["some"];
Predicate nestedPredicate = Parse(ctx, elementFilterSchema, column, elementFields, processLiterals, false);
predicates.Push(new PredicateOperand(new Predicate(
new PredicateOperand(column),
op,
new PredicateOperand(nestedPredicate)
)));
}

continue;
case "none":
op = PredicateOperation.ARRAY_NONE;

if (value is not List<ObjectFieldNode> noneFields || noneFields.Count == 0)
{
throw new DataApiBuilderException(
message: "Bad syntax: 'none' list filter requires at least one nested filter field.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}

{
IInputValueDefinition elementFilterSchema = argumentObject.Fields["none"];
Predicate nestedPredicate = Parse(ctx, elementFilterSchema, column, noneFields, processLiterals, false);
predicates.Push(new PredicateOperand(new Predicate(
new PredicateOperand(column),
op,
new PredicateOperand(nestedPredicate)
)));
}

continue;
case "all":
op = PredicateOperation.ARRAY_ALL;

if (value is not List<ObjectFieldNode> allFields || allFields.Count == 0)
{
throw new DataApiBuilderException(
message: "Bad syntax: 'all' list filter requires at least one nested filter field.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}

{
IInputValueDefinition elementFilterSchema = argumentObject.Fields["all"];
Predicate nestedPredicate = Parse(ctx, elementFilterSchema, column, allFields, processLiterals, false);
predicates.Push(new PredicateOperand(new Predicate(
new PredicateOperand(column),
op,
new PredicateOperand(nestedPredicate)
)));
}

continue;
case "any":
processLiteral = false;
op = PredicateOperation.ARRAY_ANY;
value = (bool)value ? "true" : "false";
break;
default:
throw new NotSupportedException($"Operation {name} on int type not supported.");
}
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Models/SqlQueryStructures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ public enum PredicateOperation
None,
Equal, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, NotEqual,
AND, OR, LIKE, NOT_LIKE,
IS, IS_NOT, EXISTS, ARRAY_CONTAINS, NOT_ARRAY_CONTAINS, IN
IS, IS_NOT, EXISTS, ARRAY_CONTAINS, NOT_ARRAY_CONTAINS, IN,
ARRAY_SOME, ARRAY_NONE, ARRAY_ALL, ARRAY_ANY
}

/// <summary>
Expand Down
58 changes: 58 additions & 0 deletions src/Core/Resolvers/CosmosQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ protected override string Build(PredicateOperation op)
return "ARRAY_CONTAINS";
case PredicateOperation.NOT_ARRAY_CONTAINS:
return "NOT ARRAY_CONTAINS";
case PredicateOperation.IN:
return "IN";
default:
throw new ArgumentException($"Cannot build unknown predicate operation {op}.");
}
Expand All @@ -137,6 +139,32 @@ protected override string Build(Predicate? predicate)
{
predicateString = $" {Build(predicate.Op)} ( {ResolveOperand(predicate.Left)}, {ResolveOperand(predicate.Right)})";
}
else if (predicate.Op == PredicateOperation.ARRAY_SOME ||
predicate.Op == PredicateOperation.ARRAY_NONE ||
predicate.Op == PredicateOperation.ARRAY_ALL)
{
string arrayField = ResolveOperand(predicate.Left);
string elementAlias = ArrayElementAlias(arrayField);

Predicate elementPredicate = predicate.Right?.AsPredicate()
?? throw new ArgumentException("Array element filter (some/none/all) requires a nested predicate.");
string elementPredicateStr = BuildArrayElementPredicate(elementPredicate, elementAlias);

predicateString = predicate.Op switch
{
PredicateOperation.ARRAY_SOME => $" EXISTS(SELECT VALUE 1 FROM {elementAlias} IN {arrayField} WHERE {elementPredicateStr}) ",
PredicateOperation.ARRAY_NONE => $" NOT EXISTS(SELECT VALUE 1 FROM {elementAlias} IN {arrayField} WHERE {elementPredicateStr}) ",
// ARRAY_ALL: every element matches => no element exists that does not match.
_ => $" NOT EXISTS(SELECT VALUE 1 FROM {elementAlias} IN {arrayField} WHERE NOT ({elementPredicateStr})) "
};
}
else if (predicate.Op == PredicateOperation.ARRAY_ANY)
{
string arrayField = ResolveOperand(predicate.Left);
predicateString = ResolveOperand(predicate.Right).Equals("true")
? $" ARRAY_LENGTH({arrayField}) > 0 "
: $" (NOT IS_DEFINED({arrayField}) OR ARRAY_LENGTH({arrayField}) = 0) ";
}
else if (ResolveOperand(predicate.Right).Equals(GQLFilterParser.NullStringValue))
{
// For Binary predicates:
Expand All @@ -163,6 +191,36 @@ protected override string Build(Predicate? predicate)
}
}

/// <summary>
/// Generates a deterministic iteration alias for an array element based on the array field path,
/// e.g. "c.tags" => "element_c_tags".
/// </summary>
private static string ArrayElementAlias(string arrayField)
=> "element_" + arrayField.Replace(".", "_").Replace("\"", string.Empty);

/// <summary>
/// Builds the predicate applied to each element of an array (used by some/none/all filters).
/// The element alias is bound directly to the array column reference while walking the predicate
/// tree, avoiding fragile string replacement on the rendered SQL.
/// </summary>
private string BuildArrayElementPredicate(Predicate predicate, string elementAlias)
{
// Leaf predicate: the left operand is the array column. The filter applies to the
// element value itself, so render the element alias in place of the column reference.
if (predicate.Left?.AsColumn() is not null)
{
string right = ResolveOperand(predicate.Right);
return right.Equals(GQLFilterParser.NullStringValue)
? $"{Build(predicate.Op)} IS_NULL({elementAlias})"
: $"{elementAlias} {Build(predicate.Op)} {right}";
}

// Chain predicate (AND/OR): recurse into both operands.
string left = BuildArrayElementPredicate(predicate.Left!.AsPredicate()!, elementAlias);
string rightStr = BuildArrayElementPredicate(predicate.Right!.AsPredicate()!, elementAlias);
return $"({left} {Build(predicate.Op)} {rightStr})";
}

/// <summary>
/// Resolves the operand either as a column, another predicate,
/// a SqlQueryStructure or returns it directly as string
Expand Down
Loading