Skip to content
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 &&
!string.IsNullOrEmpty(paramName) &&
value is string stringValue &&
Comment on lines +89 to +93
GetUnderlyingSourceDefinition().Columns.ContainsKey(paramName))
{
Type columnSystemType = GetColumnSystemType(paramName);
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())));

// 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());

// 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: $"Source definition 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
4 changes: 2 additions & 2 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,7 +32,6 @@ 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";
}
Expand All @@ -46,6 +45,7 @@ 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";
Comment on lines 45 to +48
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 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 Expand Up @@ -179,7 +179,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView
List<DirectiveNode> directives = new();
if (sourceDefinition.PrimaryKey.Contains(columnName))
{
directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName, new ArgumentNode("databaseType", column.SystemType.Name)));
directives.Add(new DirectiveNode(PrimaryKeyDirectiveType.DirectiveName));
}
Comment on lines 179 to 183

if (column.IsReadOnly)
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 @@ -1847,8 +1847,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)
{
Assert.Inconclusive("Test skipped for PostgreSql.");
}
}
}
Loading