Skip to content
Open
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 &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need this implementation only for PostgreSQL explicitly, please override this function into a PostgreSqlQueryStructure. If such a class doesnt exist, please create it as a derived class inheriting from BaseSqlQueryStructure.

!string.IsNullOrEmpty(paramName) &&
value is string stringValue &&
Comment on lines +89 to +93

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for non-string values, do we expect column system type to match the value type? If we cant be certain, shouldn't we do GetParamAsSystemType for kinds of values?

GetUnderlyingSourceDefinition().Columns.ContainsKey(paramName))
{
Type columnSystemType = GetColumnSystemType(paramName);
if (columnSystemType != typeof(string))
{
value = GetParamAsSystemType(stringValue, paramName, columnSystemType);
}
Comment on lines +94 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code seems redundant. can it be optimized? for e.g., GetColumnSystemType()) also calls GetUnderlyingSourceDefinition() internally.


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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by doing this arent we losing the key DB_OPERATION_RESULT_FIELD_NAME to value GetDbOperationResultField association?


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we changing this? it seems we are re-wrapping the same type again..


// 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.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new DataApiBuilderException(message: $"Source definition for entity '{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
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be all in lowercase and consistent to the above supported data types?

}

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should work now. Please keep it and make it functional

{
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