diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs
index 372005a4ab..3544f6a2da 100644
--- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs
+++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs
@@ -85,6 +85,32 @@ public BaseSqlQueryStructure(
}
}
+ ///
+ 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 &&
+ 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);
+ }
+
///
/// For UPDATE (OVERWRITE) operation
/// Adds result of (SourceDefinition.Columns minus MutationFields) to UpdateOperations with null values
@@ -422,9 +448,9 @@ protected List GenerateOutputColumns()
/// Tries to parse the string parameter to the given system type
/// Useful for inferring parameter types for columns or procedure parameters
///
- ///
- ///
- ///
+ /// The string value to parse.
+ /// The target system type for the parsed value.
+ /// The parameter parsed as the requested system type.
///
protected static object ParseParamAsSystemType(string param, Type systemType)
{
diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs
index a7b51d8827..d449c396c0 100644
--- a/src/Core/Services/GraphQLSchemaCreator.cs
+++ b/src/Core/Services/GraphQLSchemaCreator.cs
@@ -295,9 +295,9 @@ private DocumentNode GenerateSqlGraphQLObjects(RuntimeEntities entities, Diction
Dictionary> 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 roles = _authorizationResolver.GetRolesForField(entityName, field: column, operation: operation);
if (!rolesAllowedForFields.TryAdd(key: column, value: roles))
{
@@ -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())
{
@@ -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 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(),
new List(),
- fields.Values.ToImmutableList()));
+ ImmutableList.Create(GetDbOperationResultField())));
+ // Return a list of all the object types to be exposed in the schema.
List nodes = new(objectTypes.Values);
nodes.AddRange(enumTypes.Values);
return new DocumentNode(nodes);
@@ -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);
diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
index fda9bdba43..db10f9fca3 100644
--- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -1733,7 +1733,7 @@ private async Task ValidateDatabaseConnection()
///
/// 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.
///
private async Task FillSchemaForTableAsync(
string schemaName,
diff --git a/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs b/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
index c66c64a18d..1f29946fb4 100644
--- a/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
+++ b/src/Service.GraphQLBuilder/GraphQLTypes/SupportedTypes.cs
@@ -6,7 +6,7 @@ namespace Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes
///
/// 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.
///
public static class SupportedHotChocolateTypes
{
@@ -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";
}
@@ -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";
}
///
diff --git a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
index 622376dc13..2de8f1f09f 100644
--- a/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
+++ b/src/Service.GraphQLBuilder/Sql/SchemaConverter.cs
@@ -39,8 +39,8 @@ public enum AggregationType
/// Runtime config information for the table.
/// Key/Value Collection mapping entity name to the entity object,
/// currently used to lookup relationship metadata.
- /// Roles to add to authorize directive at the object level (applies to query/read ops).
- /// Roles to add to authorize directive at the field level (applies to mutations).
+ /// Roles to add to authorize directive at the object level.
+ /// Roles to add to authorize directive at the field level.
/// A GraphQL object type to be provided to a Hot Chocolate GraphQL document.
public static ObjectTypeDefinitionNode GenerateObjectTypeDefinitionForDatabaseObject(
string entityName,
@@ -179,7 +179,7 @@ private static ObjectTypeDefinitionNode CreateObjectTypeDefinitionForTableOrView
List 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));
}
if (column.IsReadOnly)
diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs
index 247685793f..8903f7ab77 100644
--- a/src/Service.Tests/Configuration/ConfigurationTests.cs
+++ b/src/Service.Tests/Configuration/ConfigurationTests.cs
@@ -1847,8 +1847,8 @@ public async Task TestSqlMetadataValidationForEntitiesWithInvalidSource()
List 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."));
}
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs
index b9459668d7..d06c5dea3c 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs
@@ -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)
@@ -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 + @"
@@ -141,15 +141,5 @@ private static string ProperlyFormatTypeTableColumn(string columnName)
return columnName;
}
}
-
- ///
- /// Bypass DateTime GQL tests for PostreSql
- ///
- [DataTestMethod]
- [Ignore]
- public new void QueryTypeColumnFilterAndOrderByDateTime(string type, string filterOperator, string sqlValue, string gqlValue, string queryOperator)
- {
- Assert.Inconclusive("Test skipped for PostgreSql.");
- }
}
}