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..e37609ae62 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,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";
}
///
- /// 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.
///
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..257b1c1eb6 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,
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.");
- }
}
}
diff --git a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
index 289454e3dc..c216b4349c 100644
--- a/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
+++ b/src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
@@ -39,7 +39,7 @@ await SetupAndRunRestApiTest(
}
///
- /// Tests the REST Api for FindByDateTimePk operation without a query string.
+ /// Tests the REST API for FindByDateTimePk operation without a query string.
///
[TestMethod]
public virtual async Task FindByDateTimePKTest()
@@ -53,7 +53,7 @@ await SetupAndRunRestApiTest(
}
///
- /// 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.
///
[TestMethod]
@@ -71,7 +71,7 @@ await SetupAndRunRestApiTest(
}
///
- /// 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
///
[TestMethod]
@@ -89,7 +89,7 @@ await SetupAndRunRestApiTest(
}
///
- /// 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
@@ -113,7 +113,7 @@ await SetupAndRunRestApiTest(
}
///
- /// Tests the Rest Api to validate that unique unicode
+ /// Tests the REST API to validate that unique unicode
/// characters work in queries.
///
///
@@ -128,7 +128,7 @@ await SetupAndRunRestApiTest(
}
///
- /// 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
@@ -240,7 +240,7 @@ await SetupAndRunRestApiTest(
///
/// 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.
///
@@ -260,7 +260,7 @@ await SetupAndRunRestApiTest(
///
/// 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.
///
diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs
index f0bbd3258a..d772100f5f 100644
--- a/src/Service.Tests/SqlTests/SqlTestBase.cs
+++ b/src/Service.Tests/SqlTests/SqlTestBase.cs
@@ -423,9 +423,9 @@ await _queryExecutor.ExecuteQueryAsync(
}
///
- /// 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.
///
/// string represents the primary key route
/// string represents the query string provided in URL