Skip to content
Draft
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
10 changes: 6 additions & 4 deletions src/Core/Models/GraphQLFilterParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ public static Predicate Parse(
predicates.Push(new PredicateOperand(new Predicate(
new PredicateOperand(column),
op,
GenerateRightOperand(ctx, argumentObject, name, processLiterals, value, processLiteral) // right operand
GenerateRightOperand(ctx, argumentObject, name, processLiterals, value, processLiteral, column.ColumnName) // right operand
)));
}

Expand Down Expand Up @@ -760,14 +760,16 @@ public static Predicate Parse(
/// <param name="processLiterals">A function to encode or parameterize literal values for database queries.</param>
/// <param name="value">The value to be used as the right operand in the predicate.</param>
/// <param name="processLiteral">Indicates whether to process the value as a literal using processLiterals, or use its string representation directly.</param>
/// <param name="columnName">The name of the column being filtered, used to look up the column's DbType for proper parameter typing.</param>
/// <returns>A <see cref="PredicateOperand"/> representing the right operand for the predicate.</returns>
private static PredicateOperand GenerateRightOperand(
IMiddlewareContext ctx,
InputObjectType argumentObject,
string operationName,
Func<object, string?, string> processLiterals,
object value,
bool processLiteral)
bool processLiteral,
string? columnName)
{
if (operationName.Equals("in", StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -777,13 +779,13 @@ private static PredicateOperand GenerateRightOperand(
argumentObject.Fields[operationName],
ctx.Variables))
.Where(inValue => inValue is not null)
.Select(inValue => processLiterals(inValue!, null))
.Select(inValue => processLiterals(inValue!, columnName))
.ToList();

return new PredicateOperand("(" + string.Join(", ", encodedParams) + ")");
}

return new PredicateOperand(processLiteral ? processLiterals(value, null) : value.ToString());
return new PredicateOperand(processLiteral ? processLiterals(value, columnName) : value.ToString());
}

private static string EscapeLikeString(string input)
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Resolvers/IQueryExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public Dictionary<string, object> GetResultProperties(
public string GetSessionParamsQuery(HttpContext? httpContext, IDictionary<string, DbConnectionParam> parameters, string dataSourceName);

/// <summary>
/// Helper method to populate DbType for parameter. Currently DbTypes for parameters are only populated for MsSql.
/// Helper method to populate DbType for parameter. Currently DbTypes for parameters are only populated for MsSql and PostgreSql.
/// </summary>
/// <param name="parameterEntry">Entry corresponding to current database parameter to be created.</param>
/// <param name="parameter">Parameter sent to database.</param>
Expand Down
47 changes: 47 additions & 0 deletions src/Core/Resolvers/PostgreSqlExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Data;
using System.Data.Common;
using Azure.Core;
using Azure.DataApiBuilder.Config;
Expand Down Expand Up @@ -146,6 +147,52 @@ private static bool ShouldManagedIdentityAccessBeAttempted(NpgsqlConnectionStrin
return string.IsNullOrEmpty(builder.Password);
}

/// <inheritdoc />
public override NpgsqlCommand PrepareDbCommand(
NpgsqlConnection conn,
string sqltext,
IDictionary<string, DbConnectionParam> parameters,
HttpContext? httpContext,
string dataSourceName)
{
NpgsqlCommand cmd = conn.CreateCommand();
cmd.CommandType = CommandType.Text;

// Add query to send user data from DAB to the underlying database to enable additional security the user might have configured
// at the database level.
string sessionParamsQuery = GetSessionParamsQuery(httpContext, parameters, dataSourceName);

cmd.CommandText = sessionParamsQuery + sqltext;
if (parameters is not null)
{
foreach (KeyValuePair<string, DbConnectionParam> parameterEntry in parameters)
{
NpgsqlParameter parameter = cmd.CreateParameter();
parameter.ParameterName = parameterEntry.Key;
parameter.Value = parameterEntry.Value.Value ?? DBNull.Value;
PopulateDbTypeForParameter(parameterEntry, parameter);
cmd.Parameters.Add(parameter);
}
}

return cmd;
}

/// <summary>
/// Populates the DbType for a PostgreSQL parameter when available.
/// This ensures proper type handling for date/time and other types,
/// preventing errors like "operator does not exist: date >= text".
/// </summary>
/// <param name="parameterEntry">The parameter entry containing the value and optional DbType.</param>
/// <param name="parameter">The DbParameter to populate.</param>
public override void PopulateDbTypeForParameter(KeyValuePair<string, DbConnectionParam> parameterEntry, DbParameter parameter)
{
if (parameterEntry.Value is not null && parameterEntry.Value.DbType is not null)
{
parameter.DbType = (DbType)parameterEntry.Value.DbType;
}
}

/// <summary>
/// Determines if the saved default azure credential's access token is valid and not expired.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Resolvers/QueryExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ public virtual string GetSessionParamsQuery(HttpContext? httpContext, IDictionar
/// <inheritdoc/>
public virtual void PopulateDbTypeForParameter(KeyValuePair<string, DbConnectionParam> parameterEntry, DbParameter parameter)
{
// DbType for parameter is currently only populated for MsSql which has its own overridden implementation.
// DbType for parameter is currently only populated for MsSql and PostgreSql which has its own overridden implementation.
return;
}

Expand Down
16 changes: 8 additions & 8 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2846,7 +2846,7 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission()
}";
string queryName = "stock_by_pk";

ValidateMutationSucceededAtDbLayer(server, client, graphQLQuery, queryName, authToken, AuthorizationResolver.ROLE_AUTHENTICATED);
await ValidateMutationSucceededAtDbLayer(server, client, graphQLQuery, queryName, authToken, AuthorizationResolver.ROLE_AUTHENTICATED);
}
finally
{
Expand Down Expand Up @@ -3168,7 +3168,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous()
/// <param name="query">GraphQL query/mutation text</param>
/// <param name="queryName">GraphQL query/mutation name</param>
/// <param name="authToken">Auth token for the graphQL request</param>
private static async void ValidateMutationSucceededAtDbLayer(TestServer server, HttpClient client, string query, string queryName, string authToken, string clientRoleHeader)
private static async Task ValidateMutationSucceededAtDbLayer(TestServer server, HttpClient client, string query, string queryName, string authToken, string clientRoleHeader)
{
JsonElement queryResponse = await GraphQLRequestExecutor.PostGraphQLRequestAsync(
client,
Expand Down Expand Up @@ -5672,12 +5672,12 @@ private static async Task<HttpStatusCode> GetGraphQLResponsePostConfigHydration(
return responseCode;
}

/// <summary>
/// Executing MCP POST requests against the engine until a non-503 error is received.
/// </summary>
/// <param name="httpClient">Client used for request execution.</param>
/// <returns>ServiceUnavailable if service is not successfully hydrated with config,
/// else the response code from the MCP request</returns>
/// <summary>
/// Executing MCP POST requests against the engine until a non-503 error is received.
/// </summary>
/// <param name="httpClient">Client used for request execution.</param>
/// <returns>ServiceUnavailable if service is not successfully hydrated with config,
/// else the response code from the MCP request</returns>
public static async Task<HttpStatusCode> GetMcpResponse(HttpClient httpClient, McpRuntimeOptions mcp)
{
// Retry request RETRY_COUNT times in exponential increments to allow
Expand Down
19 changes: 10 additions & 9 deletions src/Service.Tests/DatabaseSchema-PostgreSql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ CREATE TABLE type_table(
float_types float,
decimal_types decimal(38, 19),
boolean_types boolean,
date_types date,
datetime_types timestamp,
bytearray_types bytea,
uuid_types uuid DEFAULT gen_random_uuid ()
Expand Down Expand Up @@ -349,14 +350,14 @@ INSERT INTO bookmarks (id, bkname)
SELECT
value,
CONCAT('Test Item #' , value)
FROM
FROM
GENERATE_SERIES(1, 10000, 1) as value;

INSERT INTO mappedbookmarks (id, bkname)
SELECT
value,
CONCAT('Test Item #' , value)
FROM
FROM
GENERATE_SERIES(1, 10000, 1) as value;

INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (1, 'Incompatible GraphQL Name', 'Compatible GraphQL Name');
Expand All @@ -366,7 +367,7 @@ INSERT INTO GQLmappings(__column1, __column2, column3) VALUES (5, 'Filtered Reco
INSERT INTO publishers(id, name) VALUES (1234, 'Big Company'), (2345, 'Small Town Publisher'), (2323, 'TBD Publishing One'), (2324, 'TBD Publishing Two Ltd'), (1940, 'Policy Publisher 01'), (1941, 'Policy Publisher 02'), (1156, 'The First Publisher');
INSERT INTO clubs(id, name) VALUES (1111, 'Manchester United'), (1112, 'FC Barcelona'), (1113, 'Real Madrid');
INSERT INTO players(id, name, current_club_id, new_club_id)
VALUES
VALUES
(1, 'Cristiano Ronaldo', 1113, 1111),
(2, 'Leonel Messi', 1112, 1113);
INSERT INTO authors(id, name, birthdate) VALUES (123, 'Jelte', '2001-01-01'), (124, 'Aniruddh', '2002-02-02'), (125, 'Aniruddh', '2001-01-01'), (126, 'Aaron', '2001-01-01');
Expand Down Expand Up @@ -405,12 +406,12 @@ VALUES (1, 'Star Trek', 'SciFi', NULL), (2, 'Cinderella', 'Tales', 3001),(3,'Ún
INSERT INTO brokers("ID Number", "First Name", "Last Name") VALUES (1, 'Michael', 'Burry'), (2, 'Jordan', 'Belfort');
INSERT INTO stocks_price(categoryid, pieceid, price, is_wholesale_price) VALUES (2, 1, 100.57, True), (1, 1, 42.75, False);
INSERT INTO stocks_price(categoryid, pieceid, instant, price, is_wholesale_price) VALUES (2, 1, '2023-08-21 15:11:04', 100.57, True);
INSERT INTO type_table(id, short_types, int_types, long_types, string_types, single_types, float_types, decimal_types, boolean_types, datetime_types, bytearray_types) VALUES
(1, 1, 1, 1, '', 0.33, 0.33, 0.333333, true, '1999-01-08 10:23:54', '\xABCDEF0123'),
(2, -1, -1, -1, 'lksa;jdflasdf;alsdflksdfkldj', -9.2, -9.2, -9.292929, false, '19990108 10:23:00', '\x98AB7511AABB1234'),
(3, -32768, -2147483648, -9223372036854775808, '', -3.4E38, -1.7E308, 2.929292E-19, true, '1753-01-01 00:00:00.000', '\x00000000'),
(4, 32767, 2147483647, 9223372036854775807, 'null', 3.4E38, 1.7E308, 2.929292E-14, true, '9999-12-31 23:59:59.997', '\xFFFFFFFF'),
(5, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO type_table(id, short_types, int_types, long_types, string_types, single_types, float_types, decimal_types, boolean_types, date_types, datetime_types, bytearray_types) VALUES
(1, 1, 1, 1, '', 0.33, 0.33, 0.333333, true, '1999-01-08', '1999-01-08 10:23:54', '\xABCDEF0123'),
(2, -1, -1, -1, 'lksa;jdflasdf;alsdflksdfkldj', -9.2, -9.2, -9.292929, false, '2000-06-15', '19990108 10:23:00', '\x98AB7511AABB1234'),
(3, -32768, -2147483648, -9223372036854775808, '', -3.4E38, -1.7E308, 2.929292E-19, true, '1753-01-01', '1753-01-01 00:00:00.000', '\x00000000'),
(4, 32767, 2147483647, 9223372036854775807, 'null', 3.4E38, 1.7E308, 2.929292E-14, true, '9999-12-31', '9999-12-31 23:59:59.997', '\xFFFFFFFF'),
(5, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO type_table(id, uuid_types) values(10, 'D1D021A8-47B4-4AE4-B718-98E89C41A161');
INSERT INTO trees("treeId", species, region, height) VALUES (1, 'Tsuga terophylla', 'Pacific Northwest', '30m'), (2, 'Pseudotsuga menziesii', 'Pacific Northwest', '40m');
INSERT INTO trees("treeId", species, region, height) VALUES (4, 'test', 'Pacific Northwest', '0m');
Expand Down
Loading