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
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,32 @@ public BaseSqlQueryStructure(
}
}

/// <inheritdoc />
public override string MakeDbConnectionParam(object? value, string? paramName = null, bool lengthOverride = false)
{
if (MetadataProvider.GetDatabaseType() is DatabaseType.PostgreSQL &&
Comment thread
Aniruddh25 marked this conversation as resolved.
!string.IsNullOrEmpty(paramName) &&
value is string stringValue &&
Comment thread
ArjunNarendra marked this conversation as resolved.
Comment thread
Aniruddh25 marked this conversation as resolved.
GetUnderlyingSourceDefinition().Columns.TryGetValue(paramName, out ColumnDefinition? columnDefinition))
{
Type columnSystemType = columnDefinition.SystemType;
if (columnSystemType != typeof(string))
{
value = GetParamAsSystemType(stringValue, paramName, columnSystemType);
}

// Npgsql requires DateTime with Kind=Unspecified for 'timestamp without time zone' columns.
// ParseParamAsSystemType returns Kind=Utc (via .UtcDateTime), which causes PostgreSQL to
// apply a UTC-to-local offset during comparison, producing incorrect filter results.
if (value is DateTime dtValue && dtValue.Kind == DateTimeKind.Utc && columnSystemType == typeof(DateTime))
{
value = DateTime.SpecifyKind(dtValue, DateTimeKind.Unspecified);
}
}

return base.MakeDbConnectionParam(value, paramName, lengthOverride);
}

/// <summary>
/// For UPDATE (OVERWRITE) operation
/// Adds result of (SourceDefinition.Columns minus MutationFields) to UpdateOperations with null values
Expand Down Expand Up @@ -422,9 +448,9 @@ protected List<LabelledColumn> GenerateOutputColumns()
/// Tries to parse the string parameter to the given system type
/// Useful for inferring parameter types for columns or procedure parameters
/// </summary>
/// <param name="param"></param>
/// <param name="systemType"></param>
/// <returns></returns>
/// <param name="param">The string value to parse.</param>
/// <param name="systemType">The target system type for the parsed value.</param>
/// <returns>The parameter parsed as the requested system type.</returns>
/// <exception cref="NotSupportedException"></exception>
protected static object ParseParamAsSystemType(string param, Type systemType)
{
Expand Down
16 changes: 5 additions & 11 deletions src/Core/Services/GraphQLSchemaCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,9 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction
Dictionary<string, IEnumerable<string>> rolesAllowedForFields = new();
SourceDefinition sourceDefinition = sqlMetadataProvider.GetSourceDefinition(entityName);
bool isStoredProcedure = entity.Source.Type is EntitySourceType.StoredProcedure;
EntityActionOperation operation = isStoredProcedure ? EntityActionOperation.Execute : EntityActionOperation.Read;
foreach (string column in sourceDefinition.Columns.Keys)
{
EntityActionOperation operation = isStoredProcedure ? EntityActionOperation.Execute : EntityActionOperation.Read;
IEnumerable<string> roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: operation);
if (!rolesAllowedForFields.TryAdd(key: column, value: roles))
{
Expand All @@ -309,7 +309,6 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction
}
}

// The roles allowed for Fields are the roles allowed to READ the fields, so any role that has a read definition for the field.
// Only add objectTypeDefinition for GraphQL if it has a role definition defined for access.
if (rolesAllowedForEntity.Any())
{
Expand Down Expand Up @@ -397,23 +396,18 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction
GenerateSourceTargetLinkingObjectDefinitions(objectTypes, linkingObjectTypes);
}

// Return a list of all the object types to be exposed in the schema.
Dictionary<string, FieldDefinitionNode> fields = new();

// Add the DBOperationResult type to the schema
NameNode nameNode = new(value: GraphQLUtils.DB_OPERATION_RESULT_TYPE);
FieldDefinitionNode field = GetDbOperationResultField();

fields.TryAdd(GraphQLUtils.DB_OPERATION_RESULT_FIELD_NAME, field);

// Add the DBOperationResult type to the schema
objectTypes.Add(GraphQLUtils.DB_OPERATION_RESULT_TYPE, new ObjectTypeDefinitionNode(
location: null,
name: nameNode,
description: null,
new List<DirectiveNode>(),
new List<NamedTypeNode>(),
fields.Values.ToImmutableList()));
ImmutableList.Create(GetDbOperationResultField())));
Comment thread
Aniruddh25 marked this conversation as resolved.

// Return a list of all the object types to be exposed in the schema.
List<IDefinitionNode> nodes = new(objectTypes.Values);
nodes.AddRange(enumTypes.Values);
return new DocumentNode(nodes);
Expand Down Expand Up @@ -748,7 +742,7 @@ private static FieldDefinitionNode GetDbOperationResultField()
DocumentNode cosmosResult = GenerateCosmosGraphQLObjects(cosmosDataSourceNames, inputObjects);
DocumentNode sqlResult = GenerateSqlGraphQLObjects(sql, inputObjects);
// Create Root node with definitions from both cosmos and sql.
DocumentNode root = new(cosmosResult.Definitions.Concat(sqlResult.Definitions).ToImmutableList());
DocumentNode root = cosmosResult.WithDefinitions(cosmosResult.Definitions.Concat(sqlResult.Definitions).ToImmutableList());
Comment thread
Aniruddh25 marked this conversation as resolved.

// Merge the inputobjectType definitions from cosmos and sql onto the root.
return (root.WithDefinitions(root.Definitions.Concat(inputObjects.Values).ToImmutableList()), inputObjects);
Expand Down
10 changes: 5 additions & 5 deletions src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public virtual string GetSchemaName(string entityName)
{
if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject))
{
throw new DataApiBuilderException(message: $"Table Definition for {entityName} has not been inferred.",
throw new DataApiBuilderException(message: $"Database object for entity '{entityName}' has not been inferred.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}
Expand All @@ -176,7 +176,7 @@ public string GetDatabaseObjectName(string entityName)
{
if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject))
{
throw new DataApiBuilderException(message: $"Table Definition for {entityName} has not been inferred.",
throw new DataApiBuilderException(message: $"Database object for entity '{entityName}' has not been inferred.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}
Expand All @@ -189,7 +189,7 @@ public SourceDefinition GetSourceDefinition(string entityName)
{
if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject))
{
throw new DataApiBuilderException(message: $"Table Definition for {entityName} has not been inferred.",
throw new DataApiBuilderException(message: $"Database object for entity '{entityName}' has not been inferred.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}
Expand All @@ -202,7 +202,7 @@ public StoredProcedureDefinition GetStoredProcedureDefinition(string entityName)
{
if (!EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? databaseObject))
{
throw new DataApiBuilderException(message: $"Stored Procedure Definition for {entityName} has not been inferred.",
throw new DataApiBuilderException(message: $"Stored procedure definition for entity '{entityName}' has not been inferred.",
statusCode: HttpStatusCode.InternalServerError,
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}
Expand Down Expand Up @@ -1733,7 +1733,7 @@ private async Task ValidateDatabaseConnection()

/// <summary>
/// Using a data adapter, obtains the schema of the given table name
/// and adds the corresponding entity in the data set.
/// and adds the corresponding DataTable to the entities data set.
/// </summary>
private async Task<DataTable> FillSchemaForTableAsync(
string schemaName,
Expand Down
9 changes: 5 additions & 4 deletions src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes
/// <summary>
/// Only used to group the supported type names under a class with a relevant name.
/// The type names mentioned here are Hotchocolate scalar built in types.
/// The corresponding SQL type name may be different for e.g. UUID maps to Guid as the SQL type.
/// The corresponding SQL type name may be different for e.g. UUID maps to Guid as the .NET type.
/// </summary>
public static class SupportedHotChocolateTypes
{
Expand All @@ -32,20 +32,21 @@ public static class SupportedHotChocolateTypes
// new name so the generated schema does not depend on a deprecated scalar.
public const string BYTEARRAY_TYPE = "Base64String";
public const string DATETIME_TYPE = "DateTime";
public const string DATETIMEOFFSET_TYPE = "DateTimeOffset";
public const string LOCALTIME_TYPE = "LocalTime";
public const string TIME_TYPE = "Time";
}

/// <summary>
/// Class representing the sql datetime types supported by DAB which in addition to the sql datetime type,
/// all map to the same .NET type of DateTime and Hotchocolate scalar type of DateTime.
/// Class representing the sql datetime types supported by DAB. All types in this class
/// map to the Hotchocolate scalar type of DateTime. Most map to the .NET type of DateTime,
/// except DATETIMEOFFSET_TYPE which maps to the .NET type of DateTimeOffset.
/// </summary>
public static class SupportedDateTimeTypes
{
public const string DATE_TYPE = "date";
public const string SMALLDATETIME_TYPE = "smalldatetime";
public const string DATETIME2_TYPE = "datetime2";
public const string DATETIMEOFFSET_TYPE = "datetimeoffset";
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ public enum AggregationType
/// <param name="configEntity">Runtime config information for the table.</param>
/// <param name="entities">Key/Value Collection mapping entity name to the entity object,
/// currently used to lookup relationship metadata.</param>
/// <param name="rolesAllowedForEntity">Roles to add to authorize directive at the object level (applies to query/read ops).</param>
/// <param name="rolesAllowedForFields">Roles to add to authorize directive at the field level (applies to mutations).</param>
/// <param name="rolesAllowedForEntity">Roles to add to authorize directive at the object level.</param>
/// <param name="rolesAllowedForFields">Roles to add to authorize directive at the field level.</param>
/// <returns>A GraphQL object type to be provided to a Hot Chocolate GraphQL document.</returns>
public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseObject(
string entityName,
Expand Down
4 changes: 2 additions & 2 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1854,8 +1854,8 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource()
List<string> exceptionMessagesList = configValidator.ConfigValidationExceptions.Select(x => x.Message).ToList();
Assert.IsTrue(exceptionMessagesList.Contains("The entity Book does not have a valid source object."));
Assert.IsTrue(exceptionMessagesList.Contains("The entity Publisher does not have a valid source object."));
Assert.IsTrue(exceptionMessagesList.Contains("Table Definition for Book has not been inferred."));
Assert.IsTrue(exceptionMessagesList.Contains("Table Definition for Publisher has not been inferred."));
Assert.IsTrue(exceptionMessagesList.Contains("Database object for entity 'Book' has not been inferred."));
Assert.IsTrue(exceptionMessagesList.Contains("Database object for entity 'Publisher' has not been inferred."));
Assert.IsTrue(exceptionMessagesList.Contains("Could not infer database object for source entity: Publisher in relationship: books. Check if the entity: Publisher is correctly defined in the config."));
Assert.IsTrue(exceptionMessagesList.Contains("Could not infer database object for target entity: Book in relationship: books. Check if the entity: Book is correctly defined in the config."));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task PG_real_graphql_single_filter_expectedValues(
[DataRow(BOOLEAN_TYPE, "'false'", "false")]
[DataRow(STRING_TYPE, "lksa;jdflasdf;alsdflksdfkldj", "\"lksa;jdflasdf;alsdflksdfkldj\"")]
[DataTestMethod]
public async Task PGSQL_real_graphql_in_filter_expectedValues(
public async Task PGSQL_graphql_in_filter_expectedValues(
string type,
string sqlValue,
string gqlValue)
Expand Down Expand Up @@ -99,7 +99,7 @@ protected override string MakeQueryOnTypeTable(
string orderBy = "id",
string limit = "1")
{
string formattedSelect = limit.Equals("1") ? "SELECT to_jsonb(subq3) AS DATA" : "SELECT json_agg(to_jsonb(subq3)) AS DATA";
string formattedSelect = limit.Equals("1") ? "SELECT to_jsonb(subq3) AS DATA" : "SELECT COALESCE(json_agg(to_jsonb(subq3)), '[]'::json) AS DATA";

return @"
" + formattedSelect + @"
Expand Down Expand Up @@ -141,15 +141,5 @@ private static string ProperlyFormatTypeTableColumn(string columnName)
return columnName;
}
}

/// <summary>
/// Bypass DateTime GQL tests for PostreSql
/// </summary>
[DataTestMethod]
[Ignore]
public new void QueryTypeColumnFilterAndOrderByDateTime(string type, string filterOperator, string sqlValue, string gqlValue, string queryOperator)
Comment thread
Aniruddh25 marked this conversation as resolved.
{
Assert.Inconclusive("Test skipped for PostgreSql.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ await SetupAndRunRestApiTest(
}

/// <summary>
/// Tests the REST Api for FindByDateTimePk operation without a query string.
/// Tests the REST API for FindByDateTimePk operation without a query string.
/// </summary>
[TestMethod]
public virtual async Task FindByDateTimePKTest()
Expand All @@ -53,7 +53,7 @@ await SetupAndRunRestApiTest(
}

/// <summary>
/// Tests the REST Api for find many operation on stored procedure
/// Tests the REST API for find many operation on stored procedure
/// Stored procedure result is not necessarily json.
/// </summary>
[TestMethod]
Expand All @@ -71,7 +71,7 @@ await SetupAndRunRestApiTest(
}

/// <summary>
/// Tests the REST Api for find one operation using required parameter
/// Tests the REST API for find one operation using required parameter
/// For Find operations, parameters must be passed in query string
/// </summary>
[TestMethod]
Expand All @@ -89,7 +89,7 @@ await SetupAndRunRestApiTest(
}

/// <summary>
/// Tests the REST Api for Find operations on empty result sets
/// Tests the REST API for Find operations on empty result sets
/// 1. GET an entity with no rows (empty table)
/// 2. GET an entity with rows, filtered to none by query parameter
/// Should be a 200 response with an empty array
Expand All @@ -113,7 +113,7 @@ await SetupAndRunRestApiTest(
}

/// <summary>
/// Tests the Rest Api to validate that unique unicode
/// Tests the REST API to validate that unique unicode
/// characters work in queries.
/// </summary>
/// <returns></returns>
Expand All @@ -128,7 +128,7 @@ await SetupAndRunRestApiTest(
}

/// <summary>
/// Tests the Rest Api to validate that queries work
/// Tests the REST API to validate that queries work
/// when there is the same table name in two different
/// schemas. In this test we have two tables both
/// named magazines but with one in the schema "foo" and
Expand Down Expand Up @@ -240,7 +240,7 @@ await SetupAndRunRestApiTest(

/// <summary>
/// Validates the repsonse when both $select and $orderby query strings are
/// used with Find API reqeusts. The response is expected to contain only the
/// used with Find API reqeusts. The response is expected to contain only the
/// fields requested in $select clause.
/// This test is executed against a table.
/// </summary>
Expand All @@ -260,7 +260,7 @@ await SetupAndRunRestApiTest(

/// <summary>
/// Validates the repsonse when both $select and $orderby query strings are
/// used with Find API reqeusts. The response is expected to contain only the
/// used with Find API reqeusts. The response is expected to contain only the
/// fields requested in $select clause.
/// This test is executed against a view.
/// </summary>
Expand Down
6 changes: 3 additions & 3 deletions src/Service.Tests/SqlTests/SqlTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,9 @@ await _queryExecutor.ExecuteQueryAsync(
}

/// <summary>
/// Does the setup required to perform a test of the REST Api for both
/// MsSql and Postgress. Shared setup logic eliminates some code duplication
/// between MsSql and Postgress.
/// Does the setup required to perform a test of the REST API for both
/// MsSql and Postgres. Shared setup logic eliminates some code duplication
/// between MsSql and Postgres.
/// </summary>
/// <param name="primaryKeyRoute">string represents the primary key route</param>
/// <param name="queryString">string represents the query string provided in URL</param>
Expand Down