diff --git a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs
index 4bd1aca768..d7d900d4f9 100644
--- a/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs
+++ b/src/Core/Resolvers/Sql Query Structures/BaseSqlQueryStructure.cs
@@ -439,7 +439,14 @@ protected static object ParseParamAsSystemType(string param, Type systemType)
"Double" => double.Parse(param),
"Decimal" => decimal.Parse(param),
"Boolean" => bool.Parse(param),
- "DateTime" => DateTimeOffset.Parse(param, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal).DateTime,
+ // When GraphQL input specifies a TZ offset "12-31-2024T12:00:00+03:00"
+ // and DAB has resolved the ColumnDefinition.SystemType to DateTime,
+ // DAB converts the input to UTC because:
+ // - DAB assumes that values without timezone offset are UTC.
+ // - DAB shouldn't store values in the backend database which the user did not intend
+ // - e.g. Storing this value for the original example would be incorrect: "12-31-2024T12:00:00"
+ // - The correct value to store would be "12-31-2024T09:00:00" (UTC)
+ "DateTime" => DateTimeOffset.Parse(param, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal).UtcDateTime,
"DateTimeOffset" => DateTimeOffset.Parse(param, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal),
"Date" => DateOnly.Parse(param),
"Guid" => Guid.Parse(param),
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/DabField.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/DabField.cs
new file mode 100644
index 0000000000..344b659041
--- /dev/null
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/DabField.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLSupportedTypesTests;
+
+///
+/// Encapsulates field name metadata which is used when
+/// creating database queries to validate test results
+/// in the GraphQLSupportedTypesTestBase.
+///
+public class DabField
+{
+ ///
+ /// Mapped (aliased) column name defined in DAB runtime config.
+ ///
+ public string Alias { get; set; }
+
+ ///
+ /// Database column name.
+ ///
+ public string BackingColumnName { get; set; }
+
+ ///
+ /// Creates a new DabField instance with both alias and backing column name.
+ ///
+ /// Mapped (aliased) column name defined in DAB runtime config.
+ /// Database column name.
+ public DabField(string alias, string backingColumnName)
+ {
+ Alias = alias;
+ BackingColumnName = backingColumnName;
+ }
+
+ ///
+ /// Creates a new DabField instance with only the backing column name
+ /// where the alias is the same as the backing column name.
+ ///
+ /// Database column name.
+ public DabField(string backingColumnName)
+ {
+ Alias = backingColumnName;
+ BackingColumnName = backingColumnName;
+ }
+}
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs
index d2626484fa..1ad5bbc93e 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/GraphQLSupportedTypesTestsBase.cs
@@ -97,9 +97,11 @@ public async Task QueryTypeColumn(string type, int id)
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "supportedType_by_pk";
- string gqlQuery = "{ supportedType_by_pk(typeid: " + id + ") { " + field + " } }";
+ string gqlQuery = "{ supportedType_by_pk(typeid: " + id + ") { typeid, " + field + " } }";
- string dbQuery = MakeQueryOnTypeTable(new List { field }, id);
+ string dbQuery = MakeQueryOnTypeTable(
+ queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) },
+ id: id);
JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: false);
string expected = await GetDatabaseResultAsync(dbQuery);
@@ -134,12 +136,6 @@ public async Task QueryTypeColumn(string type, int id)
[DataRow(LONG_TYPE, "eq", "-1", "-1", "=")]
[DataRow(STRING_TYPE, "neq", "'foo'", "\"foo\"", "!=")]
[DataRow(STRING_TYPE, "eq", "'lksa;jdflasdf;alsdflksdfkldj'", "\"lksa;jdflasdf;alsdflksdfkldj\"", "=")]
- [DataRow(SINGLE_TYPE, "gt", "-9.3", "-9.3", ">")]
- [DataRow(SINGLE_TYPE, "gte", "-9.2", "-9.2", ">=")]
- [DataRow(SINGLE_TYPE, "lt", ".33", "0.33", "<")]
- [DataRow(SINGLE_TYPE, "lte", ".33", "0.33", "<=")]
- [DataRow(SINGLE_TYPE, "neq", "9.2", "9.2", "!=")]
- [DataRow(SINGLE_TYPE, "eq", "'0.33'", "0.33", "=")]
[DataRow(FLOAT_TYPE, "gt", "-9.2", "-9.2", ">")]
[DataRow(FLOAT_TYPE, "gte", "-9.2", "-9.2", ">=")]
[DataRow(FLOAT_TYPE, "lt", ".33", "0.33", "<")]
@@ -166,14 +162,20 @@ public async Task QueryTypeColumnFilterAndOrderBy(string type, string filterOper
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "supportedTypes";
string gqlQuery = @"{
- supportedTypes(first: 100 orderBy: { " + field + ": ASC } filter: { " + field + ": {" + filterOperator + ": " + gqlValue + @"} }) {
+ supportedTypes(first: 100 orderBy: { typeid: ASC } filter: { " + field + ": {" + filterOperator + ": " + gqlValue + @"} }) {
items {
- " + field + @"
+ typeid, " + field + @"
}
}
}";
- string dbQuery = MakeQueryOnTypeTable(new List { field }, filterValue: sqlValue, filterOperator: queryOperator, filterField: field, orderBy: field, limit: "100");
+ string dbQuery = MakeQueryOnTypeTable(
+ queryFields: new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) },
+ filterValue: sqlValue,
+ filterOperator: queryOperator,
+ filterField: field,
+ orderBy: "id",
+ limit: "100");
JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: false);
string expected = await GetDatabaseResultAsync(dbQuery);
@@ -215,8 +217,8 @@ public async Task QueryTypeColumnFilterAndOrderBy(string type, string filterOper
DisplayName = "datetime type filter and orderby test with neq operator and specific value '1999-01-08 10:23:54'.")]
[DataRow(DATETIMEOFFSET_TYPE, "neq", "'1999-01-08 10:23:54.9999999-14:00'", "\"1999-01-08 10:23:54.9999999-14:00\"", "!=",
DisplayName = "datetimeoffset type filter and orderby test with neq operator")]
- [DataRow(DATETIMEOFFSET_TYPE, "lt", "'9999-12-31 23:59:59.9999999'", "\"9999-12-31 23:59:59.9999999\"", "<",
- DisplayName = "datetimeoffset type filter and orderby test with lt operator and max value for datetimeoffset.")]
+ [DataRow(DATETIMEOFFSET_TYPE, "lt", "'9999-12-31 23:59:59.9999999'", "\"9999-12-31 23:59:59.9999999Z\"", "<",
+ DisplayName = "datetimeoffset type filter and orderby test with lt operator and max value (with UTC offset specified) for datetimeoffset.")]
[DataRow(DATETIMEOFFSET_TYPE, "eq", "'1999-01-08 10:23:54.9999999-14:00'", "\"1999-01-08 10:23:54.9999999-14:00\"", "=",
DisplayName = "datetimeoffset type filter and orderby test with eq operator")]
[DataRow(DATE_TYPE, "eq", "'1999-01-08'", "\"1999-01-08\"", "=",
@@ -245,10 +247,11 @@ public async Task QueryTypeColumnFilterAndOrderBy(string type, string filterOper
DisplayName = "datetime2 type filter and orderby test with neq operator")]
public async Task QueryTypeColumnFilterAndOrderByDateTime(string type, string filterOperator, string sqlValue, string gqlValue, string queryOperator)
{
- // In MySQL, the DATETIME data type supports a range from '1000-01-01 00:00:00.0000000' to '9999-12-31 23:59:59.0000000'
+ // In MySQL, the DATETIME data type supports a range from '1000-01-01 00:00:00.0000000' to '9999-12-31 23:59:59.499999'
+ // https://dev.mysql.com/doc/refman/8.4/en/datetime.html
if (DatabaseEngine is TestCategory.MYSQL && sqlValue is "'9999-12-31 23:59:59.9999999'")
{
- sqlValue = "'9999-12-31 23:59:59.0000000'";
+ sqlValue = "'9999-12-31 23:59:59.499999";
gqlValue = "\"9999-12-31 23:59:59.0000000\"";
}
@@ -271,36 +274,68 @@ public async Task QueryTypeColumnFilterAndOrderByLocalTime(string type, string f
}
///
+ /// (MSSQL Test, which supports time type with 7 decimal precision)
/// Validates that LocalTime values with X precision are handled correctly: precision of 7 decimal places used with eq (=) will
/// not return result with only 3 decimal places i.e. 10:23:54.999 != 10:23:54.9999999
- /// In the Database only one row exist with value 23:59:59.9999999
+ /// In the Database only one row exists with value 23:59:59.9999999
///
[DataTestMethod]
- [DataRow("\"23:59:59.9999999\"", 1, DisplayName = "TimeType Precision Check with 7 decimal places")]
- [DataRow("\"23:59:59.999\"", 0, DisplayName = "TimeType Precision Check with 3 decimal places")]
- public async Task TestTimeTypePrecisionCheck(string gqlValue, int count)
+ [DataRow("23:59:59.9999999", DisplayName = "TimeType Precision Check with 7 decimal places")]
+ [DataRow("23:59:59.999", DisplayName = "TimeType Precision Check with 3 decimal places")]
+ public async Task TestTimeTypePrecisionCheck(string gqlInput)
{
+ // Arrange
if (!IsSupportedType(TIME_TYPE))
{
Assert.Inconclusive("Type not supported");
}
+ string graphQLMutationName = "createSupportedType";
+ string gqlMutation = "mutation{ createSupportedType (item: { time_types: \"" + gqlInput + "\"}){ typeid, time_types } }";
+ JsonElement testScopedInsertedRecord = await ExecuteGraphQLRequestAsync(gqlMutation, graphQLMutationName, isAuthenticated: true);
+
+ if (!testScopedInsertedRecord.TryGetProperty("typeid", out JsonElement typeid))
+ {
+ Assert.Fail(message: "Failed to insert test record.");
+ }
+
+ int insertedRecordId = typeid.GetInt32();
+
string graphQLQueryName = "supportedTypes";
+ string idFilter = @"typeid: { eq: " + typeid.GetInt32() + " }";
string gqlQuery = @"{
- supportedTypes(first: 100 orderBy: { " + "time_types" + ": ASC } filter: { " + "time_types" + ": {" + "eq" + ": " + gqlValue + @"} }) {
+ supportedTypes(
+ orderBy: { " + "time_types" + @": ASC }
+ filter: { " + "time_types" + ": {" + "eq" + ": \"" + gqlInput + "\"}," + idFilter + @"}) {
items {
- " + "time_types" + @"
+ typeid, " + "time_types" + @"
}
}
}";
+ // Act - Execute query with filter on time_types, expect to get back time_types with 7 decimal places.
JsonElement gqlResponse = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: false);
- Assert.AreEqual(count, gqlResponse.GetProperty("items").GetArrayLength());
+
+ // Assert
+ // Validate that the filter returned a result.
+ bool receivedResultWithFilter = gqlResponse.TryGetProperty("items", out JsonElement items) && items.GetArrayLength() > 0;
+ Assert.AreEqual(expected: true, actual: receivedResultWithFilter, message: "Unexpected results and result count.");
+
+ // Validate that the filter returned the value inserted during setup.
+ JsonElement firstItem = items[0];
+ JsonElement typeId = firstItem.GetProperty("typeid");
+ int actualTypeIdInResponse = typeId.GetInt32();
+ Assert.AreEqual(expected: insertedRecordId, actual: actualTypeIdInResponse, message: "typeid mismatch, filter didn't work");
+
+ // Validate that the filter returned the value with expected time_types precision.
+ JsonElement timeValue = firstItem.GetProperty("time_types");
+ string actualTimeValue = timeValue.GetString();
+ Assert.AreEqual(expected: gqlInput, actual: actualTimeValue, message: "returned time value unexpected.");
}
///
- /// The method constructs a GraphQL query to insert the value into the database table
- /// and then executes the query and compares the expected result with the actual result to verify if different types are supported.
+ /// This test executes a GraphQL insert mutation for each data row's {value} of GraphQL data type {type}
+ /// to validate that the DAB engine supports inserting different types into the database.
///
[DataTestMethod]
[DataRow(BYTE_TYPE, "255")]
@@ -314,7 +349,7 @@ public async Task TestTimeTypePrecisionCheck(string gqlValue, int count)
[DataRow(INT_TYPE, "0")]
[DataRow(INT_TYPE, "-9999")]
[DataRow(INT_TYPE, "null")]
- [DataRow(UUID_TYPE, "3a1483a5-9ac2-4998-bcf3-78a28078c6ac")]
+ [DataRow(UUID_TYPE, "\"3a1483a5-9ac2-4998-bcf3-78a28078c6ac\"")]
[DataRow(UUID_TYPE, "null")]
[DataRow(LONG_TYPE, "0")]
[DataRow(LONG_TYPE, "9000000000000000000")]
@@ -345,9 +380,9 @@ public async Task TestTimeTypePrecisionCheck(string gqlValue, int count)
[DataRow(TIME_TYPE, "\"23:59\"")]
[DataRow(TIME_TYPE, "null")]
[DataRow(DATETIME_TYPE, "\"1753-01-01 00:00:00.000\"")]
- [DataRow(DATETIME_TYPE, "\"9999-12-31 23:59:59.997\"")]
- [DataRow(DATETIME_TYPE, "\"9999-12-31T23:59:59.997\"")]
- [DataRow(DATETIME_TYPE, "\"9999-12-31 23:59:59.997Z\"")]
+ [DataRow(DATETIME_TYPE, "\"9999-12-31 23:59:59.499\"")]
+ [DataRow(DATETIME_TYPE, "\"9999-12-31T23:59:59.499\"")]
+ [DataRow(DATETIME_TYPE, "\"9999-12-31 23:59:59.499Z\"")]
[DataRow(DATETIME_TYPE, "null")]
[DataRow(SMALLDATETIME_TYPE, "\"1900-01-01\"")]
[DataRow(SMALLDATETIME_TYPE, "\"2079-06-06\"")]
@@ -369,17 +404,17 @@ public async Task InsertIntoTypeColumn(string type, string value)
}
string field = $"{type.ToLowerInvariant()}_types";
- string graphQLQueryName = "createSupportedType";
- string gqlQuery = "mutation{ createSupportedType (item: {" + field + ": " + value + " }){ " + field + " } }";
-
- string dbQuery = MakeQueryOnTypeTable(new List { field }, id: 5001);
+ string graphQLMutationName = "createSupportedType";
+ string gqlMutation = "mutation{ createSupportedType (item: {" + field + ": " + value + " }){ typeid, " + field + " } }";
- JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: true);
- string expected = await GetDatabaseResultAsync(dbQuery);
+ JsonElement actual = await ExecuteGraphQLRequestAsync(gqlMutation, graphQLMutationName, isAuthenticated: true);
+ Assert.IsTrue(
+ condition: actual.GetProperty("typeid").TryGetInt32(out int insertedRecordId),
+ message: "Error: GraphQL mutation result indicates issue during record creation.");
+ string expectedResultDbQuery = MakeQueryOnTypeTable(new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, id: insertedRecordId);
+ string expectedResult = await GetDatabaseResultAsync(expectedResultDbQuery);
- PerformTestEqualsForExtendedTypes(type, expected, actual.ToString());
-
- await ResetDbStateAsync();
+ PerformTestEqualsForExtendedTypes(type, expectedResult, actual.ToString());
}
///
@@ -401,7 +436,7 @@ public async Task InsertInvalidTimeIntoTimeTypeColumn(string type, string value)
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "createSupportedType";
- string gqlQuery = "mutation{ createSupportedType (item: {" + field + ": " + value + " }){ " + field + " } }";
+ string gqlQuery = "mutation{ createSupportedType (item: {" + field + ": " + value + " }){ typeid, " + field + " } }";
JsonElement response = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: true);
string responseMessage = Regex.Unescape(JsonSerializer.Serialize(response));
@@ -426,8 +461,7 @@ public async Task InsertInvalidTimeIntoTimeTypeColumn(string type, string value)
[DataRow(BOOLEAN_TYPE, true)]
[DataRow(DATETIMEOFFSET_TYPE, "1999-01-08 10:23:54+8:00")]
[DataRow(DATETIME_TYPE, "1999-01-08 10:23:54")]
- [DataRow(TIME_TYPE, "\"23:59:59.9999999\"")]
- [DataRow(TIME_TYPE, "null")]
+ [DataRow(TIME_TYPE, "23:59:59")]
[DataRow(BYTEARRAY_TYPE, "V2hhdGNodSBkb2luZyBkZWNvZGluZyBvdXIgdGVzdCBiYXNlNjQgc3RyaW5ncz8=")]
[DataRow(UUID_TYPE, "3a1483a5-9ac2-4998-bcf3-78a28078c6ac")]
public async Task InsertIntoTypeColumnWithArgument(string type, object value)
@@ -439,16 +473,16 @@ public async Task InsertIntoTypeColumnWithArgument(string type, object value)
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "createSupportedType";
- string gqlQuery = "mutation($param: " + TypeNameToGraphQLType(type) + "){ createSupportedType (item: {" + field + ": $param }){ " + field + " } }";
-
- string dbQuery = MakeQueryOnTypeTable(new List { field }, id: 5001);
+ string gqlQuery = "mutation($param: " + TypeNameToGraphQLType(type) + "){ createSupportedType (item: {" + field + ": $param }){ typeid, " + field + " } }";
JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: true, new() { { "param", value } });
- string expected = await GetDatabaseResultAsync(dbQuery);
-
- PerformTestEqualsForExtendedTypes(type, expected, actual.ToString());
+ Assert.IsTrue(
+ condition: actual.GetProperty("typeid").TryGetInt32(out int insertedRecordId),
+ message: "Error: GraphQL mutation result indicates issue during record creation.");
+ string expectedResultDbQuery = MakeQueryOnTypeTable(new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, id: insertedRecordId);
+ string expectedResult = await GetDatabaseResultAsync(expectedResultDbQuery);
- await ResetDbStateAsync();
+ PerformTestEqualsForExtendedTypes(type, expectedResult, actual.ToString());
}
[DataTestMethod]
@@ -505,16 +539,14 @@ public async Task UpdateTypeColumn(string type, string value)
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "updateSupportedType";
- string gqlQuery = "mutation{ updateSupportedType (typeid: 1, item: {" + field + ": " + value + " }){ " + field + " } }";
+ string gqlQuery = "mutation{ updateSupportedType (typeid: 1, item: {" + field + ": " + value + " }){ typeid " + field + " } }";
- string dbQuery = MakeQueryOnTypeTable(new List { field }, id: 1);
+ string dbQuery = MakeQueryOnTypeTable(new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, id: 1);
JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: true);
string expected = await GetDatabaseResultAsync(dbQuery);
PerformTestEqualsForExtendedTypes(type, expected, actual.ToString());
-
- await ResetDbStateAsync();
}
[DataTestMethod]
@@ -547,16 +579,14 @@ public async Task UpdateTypeColumnWithArgument(string type, object value)
string field = $"{type.ToLowerInvariant()}_types";
string graphQLQueryName = "updateSupportedType";
- string gqlQuery = "mutation($param: " + TypeNameToGraphQLType(type) + "){ updateSupportedType (typeid: 1, item: {" + field + ": $param }){ " + field + " } }";
+ string gqlQuery = "mutation($param: " + TypeNameToGraphQLType(type) + "){ updateSupportedType (typeid: 1, item: {" + field + ": $param }){ typeid, " + field + " } }";
- string dbQuery = MakeQueryOnTypeTable(new List { field }, id: 1);
+ string dbQuery = MakeQueryOnTypeTable(new List { new(alias: "typeid", backingColumnName: "id"), new(backingColumnName: field) }, id: 1);
JsonElement actual = await ExecuteGraphQLRequestAsync(gqlQuery, graphQLQueryName, isAuthenticated: true, new() { { "param", value } });
string expected = await GetDatabaseResultAsync(dbQuery);
PerformTestEqualsForExtendedTypes(type, expected, actual.ToString());
-
- await ResetDbStateAsync();
}
#endregion
@@ -565,7 +595,7 @@ public async Task UpdateTypeColumnWithArgument(string type, object value)
/// Utility function to do special comparisons for some of the extended types
/// if json compare doesn't suffice
///
- private static void PerformTestEqualsForExtendedTypes(string type, string expected, string actual)
+ protected static void PerformTestEqualsForExtendedTypes(string type, string expected, string actual)
{
switch (type)
{
@@ -833,23 +863,24 @@ private static void AssertOnFields(string field, string actualElement, string ex
///
private static string TypeNameToGraphQLType(string typeName)
{
- if (typeName is DATETIMEOFFSET_TYPE)
+ return typeName switch
{
- return DATETIME_TYPE;
- }
-
- return typeName;
+ DATETIMEOFFSET_TYPE => DATETIME_TYPE,
+ TIME_TYPE => LOCALTIME_TYPE,
+ _ => typeName
+ };
}
protected abstract string MakeQueryOnTypeTable(
- List queriedColumns,
+ List queryFields,
string filterValue = "1",
string filterOperator = "=",
string filterField = "1",
string orderBy = "id",
string limit = "1");
- protected abstract string MakeQueryOnTypeTable(List columnsToQuery, int id);
+ protected abstract string MakeQueryOnTypeTable(List queryFields, int id);
+
protected virtual bool IsSupportedType(string type)
{
return true;
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MsSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MsSqlGQLSupportedTypesTests.cs
index aab48628bd..66832b958c 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MsSqlGQLSupportedTypesTests.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MsSqlGQLSupportedTypesTests.cs
@@ -2,8 +2,10 @@
// Licensed under the MIT License.
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedHotChocolateTypes;
namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLSupportedTypesTests
{
@@ -21,13 +23,38 @@ public static async Task SetupAsync(TestContext context)
await InitializeTestFixture();
}
- protected override string MakeQueryOnTypeTable(List columnsToQuery, int id)
+ ///
+ /// MSSQL Single Type Tests.
+ ///
+ /// GraphQL Type
+ /// Comparison operator: gt, lt, gte, lte, etc.
+ /// Value to be set in "expected value" sql query.
+ /// GraphQL input value supplied.
+ /// Query operator for "expected value" sql query.
+ [DataRow(SINGLE_TYPE, "gt", "-9.3", "-9.3", ">")]
+ [DataRow(SINGLE_TYPE, "gte", "-9.2", "-9.2", ">=")]
+ [DataRow(SINGLE_TYPE, "lt", ".33", "0.33", "<")]
+ [DataRow(SINGLE_TYPE, "lte", ".33", "0.33", "<=")]
+ [DataRow(SINGLE_TYPE, "neq", "9.2", "9.2", "!=")]
+ [DataRow(SINGLE_TYPE, "eq", "'0.33'", "0.33", "=")]
+ [DataTestMethod]
+ public async Task MSSQL_real_graphql_single_filter_expectedValues(
+ string type,
+ string filterOperator,
+ string sqlValue,
+ string gqlValue,
+ string queryOperator)
+ {
+ await QueryTypeColumnFilterAndOrderBy(type, filterOperator, sqlValue, gqlValue, queryOperator);
+ }
+
+ protected override string MakeQueryOnTypeTable(List queryFields, int id)
{
- return MakeQueryOnTypeTable(columnsToQuery, filterValue: id.ToString(), filterField: "id");
+ return MakeQueryOnTypeTable(queryFields, filterValue: id.ToString(), filterField: "id");
}
protected override string MakeQueryOnTypeTable(
- List queriedColumns,
+ List queryFields,
string filterValue = "1",
string filterOperator = "=",
string filterField = "1",
@@ -36,7 +63,7 @@ protected override string MakeQueryOnTypeTable(
{
string format = limit.Equals("1") ? "WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES" : "INCLUDE_NULL_VALUES";
return @"
- SELECT TOP " + limit + " " + string.Join(", ", queriedColumns) + @"
+ SELECT TOP " + limit + " " + string.Join(", ", queryFields.Select(field => $"{field.BackingColumnName} AS {field.Alias}")) + @"
FROM type_table AS [table0]
WHERE " + filterField + " " + filterOperator + " " + filterValue + @"
ORDER BY " + orderBy + @" asc
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs
index 36f47fcb43..04ce926ad6 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/MySqlGQLSupportedTypesTests.cs
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes.SupportedDateTimeTypes;
@@ -24,27 +26,135 @@ public static async Task SetupAsync(TestContext context)
await InitializeTestFixture();
}
- protected override string MakeQueryOnTypeTable(List columnsToQuery, int id)
+ ///
+ /// Validates that GraphQL mutation input for:
+ /// GraphQL Field Name (Type): datetime_types (DateTime)
+ /// MySQL Field Name (Type): datetime_types (datetime)
+ /// succeeds when the input is a valid DateTime string as described by MySQL documentation.
+ /// The field under test is of MySQL type datetime, with no fractional second precision.
+ /// MySQL converts the supplied datetime value to UTC before storing it in the database.
+ ///
+ ///
+ /// The DATETIME type is used for values that contain both date and time parts.
+ /// MySQL retrieves and displays DATETIME values in 'YYYY-MM-DD hh:mm:ss' format.
+ /// The supported range is '1000-01-01 00:00:00' to '9999-12-31 23:59:59'.
+ ///
+ ///
+ /// Inserting a TIME, DATE, or TIMESTAMP value with a fractional seconds part into a column
+ /// of the same type but having fewer fractional digits results in rounding.
+ ///
+ ///
+ /// - The (time zone) offset is not displayed when selecting a datetime value,
+ /// even if one was used when inserting it.
+ /// - The date and time parts can be separated by T rather than a space.
+ /// For example, '2012-12-31 11:30:45' '2012-12-31T11:30:45' are equivalent.
+ ///
+ ///
+ /// UTC offset should be formatted as Z and not +00:00 and lower case z and t should be converted to Z and T.
+ ///
+ /// Unescaped string used as value for GraphQL input field datetime_types
+ /// Expected result the HotChocolate returns from resolving database response.
+ // Date and time
+ [DataRow("1000-01-01 00:00:00", "1000-01-01T00:00:00.000Z", DisplayName = "Datetime value separated by space.")]
+ [DataRow("9999-12-31T23:59:59", "9999-12-31T23:59:59.000Z", DisplayName = "Datetime value separated by T.")]
+ [DataRow("9999-12-31 23:59:59Z", "9999-12-31T23:59:59.000Z", DisplayName = "Datetime value specified with UTC offset Z as resolved by HotChocolate.")]
+ [DataRow("9999-12-31 23:59:59+00:00", "9999-12-31T23:59:59.000Z", DisplayName = "Datetime value specified with UTC offset with no datetime change when stored in db.")]
+ [DataRow("9999-12-31 23:59:59+03:00", "9999-12-31T20:59:59.000Z", DisplayName = "Timezone offset UTC+03:00 accepted by MySQL because UTC value is in supported datetime range.")]
+ [DataRow("9999-12-31 20:59:59-03:00", "9999-12-31T23:59:59.000Z", DisplayName = "Timezone offset UTC-03:00 accepted by MySQL because UTC value is in supported datetime range.")]
+ // Fractional seconds rounded up/down when mysql column datetime doesn't specify fractional seconds
+ // e.g. column not defined as datetime({1-6})
+ [DataRow("9999-12-31 23:59:59.499999", "9999-12-31T23:59:59.000Z", DisplayName = "Fractional seconds rounded down because fractional seconds are passed to column with datatype datetime(0).")]
+ [DataRow("2024-12-31 23:59:59.999999", "2025-01-01T00:00:00.000Z", DisplayName = "Fractional seconds rounded up because fractional seconds are passed to column with datatype datetime(0).")]
+ // Only date
+ [DataRow("9999-12-31", "9999-12-31T00:00:00.000Z", DisplayName = "Max date for datetime column stored with zeroed out time.")]
+ [DataRow("1000-01-01", "1000-01-01T00:00:00.000Z", DisplayName = "Min date for datetime column stored with zeroed out time.")]
+ [DataTestMethod]
+ public async Task InsertMutationInput_DateTimeTypes_ValidRange_ReturnsExpectedValues(string dateTimeGraphQLInput, string expectedResult)
+ {
+ // Arrange
+ const string DATETIME_FIELD = "datetime_types";
+ string graphQLMutationName = "createSupportedType";
+ string gqlMutation = "mutation{ createSupportedType (item: {" + DATETIME_FIELD + ": \"" + dateTimeGraphQLInput + "\" }){ typeid, " + DATETIME_FIELD + " } }";
+
+ // Act
+ JsonElement actual = await ExecuteGraphQLRequestAsync(gqlMutation, graphQLMutationName, isAuthenticated: true);
+
+ // Assert
+ Assert.IsTrue(
+ condition: actual.GetProperty("typeid").TryGetInt32(out _),
+ message: "Error: GraphQL mutation result indicates issue during record creation because primary key 'typeid' was not resolved.");
+
+ Assert.AreEqual(
+ expected: expectedResult,
+ actual: actual.GetProperty(DATETIME_FIELD).GetString(),
+ message: "Unexpected datetime value.");
+ }
+
+ ///
+ /// For MySQL, values passed to columns with datatype datetime(0) (no fractional seconds) that only include time
+ /// are auto-populated with the current date.
+ /// Based on the supplied time (assumed to be UTC), gets the current date. This calculation must fetch
+ /// DateTime.UtcNow, otherwise the test machine's timezone will be used and result in an unexpected date.
+ ///
+ /// Unescaped string used as value for GraphQL input field datetime_types
+ /// Expected result the HotChocolate returns from resolving database response.
+ [DataRow("23:59:59.499999", "23:59:59.000Z", DisplayName = "hh:mm::ss.ffffff for datetime column stored with zeroed out date and rounded down fractional seconds.")]
+ [DataRow("23:59:59", "23:59:59.000Z", DisplayName = "hh:mm:ss for datetime column stored with zeroed out date.")]
+ [DataRow("23:59", "23:59:00.000Z", DisplayName = "hh:mm for datetime column stored with zeroed out date and seconds.")]
+ [DataTestMethod]
+ public async Task InsertMutationInput_DateTimeTypes_TimeOnly_ValidRange_ReturnsExpectedValues(string dateTimeGraphQLInput, string expectedResult)
+ {
+ // Arrange
+ expectedResult = DateTime.UtcNow.ToString("yyyy-MM-ddT") + expectedResult;
+ await InsertMutationInput_DateTimeTypes_ValidRange_ReturnsExpectedValues(dateTimeGraphQLInput, expectedResult);
+ }
+
+ ///
+ /// MySql Single Type Tests.
+ ///
+ /// GraphQL Data Type
+ /// Comparison operator: gt, lt, gte, lte, etc.
+ /// Value to be set in "expected value" sql query.
+ /// GraphQL input value supplied.
+ /// Query operator for "expected value" sql query.
+ [DataRow(SINGLE_TYPE, "gt", "-9.3", "-9.3", ">")]
+ [DataRow(SINGLE_TYPE, "gte", "-9.2", "-9.2", ">=")]
+ [DataRow(SINGLE_TYPE, "lt", ".33", "0.33", "<")]
+ [DataRow(SINGLE_TYPE, "lte", ".33", "0.33", "<=")]
+ [DataRow(SINGLE_TYPE, "neq", "9.2", "9.2", "!=")]
+ [DataRow(SINGLE_TYPE, "eq", "'0.33'", "0.33", "=")]
+ [DataTestMethod]
+ public async Task MySql_real_graphql_single_filter_expectedValues(
+ string graphqlDataType,
+ string filterOperator,
+ string sqlValue,
+ string gqlValue,
+ string queryOperator)
+ {
+ await QueryTypeColumnFilterAndOrderBy(graphqlDataType, filterOperator, sqlValue, gqlValue, queryOperator);
+ }
+
+ protected override string MakeQueryOnTypeTable(List queryFields, int id)
{
- return MakeQueryOnTypeTable(columnsToQuery, filterValue: id.ToString(), filterField: "id");
+ return MakeQueryOnTypeTable(queryFields, filterValue: id.ToString(), filterField: "id");
}
protected override string MakeQueryOnTypeTable(
- List queriedColumns,
+ List queryFields,
string filterValue = "1",
string filterOperator = "=",
string filterField = "1",
string orderBy = "id",
string limit = "1")
{
- string columnsToQuery = string.Join(", ", queriedColumns.Select(c => $"\"{c}\" , {ProperlyFormatTypeTableColumn(c)}"));
- string formattedSelect = limit.Equals("1") ? "SELECT JSON_OBJECT(" + columnsToQuery + @") AS `data`" :
- "SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(" + columnsToQuery + @")), '[]') AS `data`";
+ string jsonResultProperties = string.Join(", ", queryFields.Select(field => $"\"{field.Alias}\" , {ProperlyFormatTypeTableColumn(field.BackingColumnName)}"));
+ string formattedSelect = limit.Equals("1") ? "SELECT JSON_OBJECT(" + jsonResultProperties + @") AS `data`" :
+ "SELECT COALESCE(JSON_ARRAYAGG(JSON_OBJECT(" + jsonResultProperties + @")), '[]') AS `data`";
return @"
" + formattedSelect + @"
FROM (
- SELECT " + string.Join(", ", queriedColumns) + @"
+ SELECT " + string.Join(", ", queryFields.Select(field => field.BackingColumnName)) + @"
FROM type_table AS `table0`
WHERE " + filterField + " " + filterOperator + " " + filterValue + @"
ORDER BY " + orderBy + @" asc
@@ -63,6 +173,7 @@ protected override bool IsSupportedType(string type)
DATETIME2_TYPE => false,
DATETIMEOFFSET_TYPE => false,
TIME_TYPE => false,
+ LOCALTIME_TYPE => false,
_ => true
};
}
diff --git a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs
index 79e5684b83..dc1e8e7d51 100644
--- a/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs
+++ b/src/Service.Tests/SqlTests/GraphQLSupportedTypesTests/PostgreSqlGQLSupportedTypesTests.cs
@@ -24,13 +24,39 @@ public static async Task SetupAsync(TestContext context)
await InitializeTestFixture();
}
- protected override string MakeQueryOnTypeTable(List columnsToQuery, int id)
+ ///
+ /// Postgres Single Type Test.
+ /// Postgres requires conversion of a float value, ex: 0.33 to 'real' type otherwise precision is lost.
+ ///
+ /// GraphQL Type
+ /// Comparison operator: gt, lt, gte, lte, etc.
+ /// Value to be set in "expected value" sql query.
+ /// GraphQL input value supplied.
+ /// Query operator for "expected value" sql query.
+ [DataRow(SINGLE_TYPE, "gt", "real '-9.3'", "-9.3", ">")]
+ [DataRow(SINGLE_TYPE, "gte", "real '-9.2'", "-9.2", ">=")]
+ [DataRow(SINGLE_TYPE, "lt", "real '.33'", "0.33", "<")]
+ [DataRow(SINGLE_TYPE, "lte", "real '.33'", "0.33", "<=")]
+ [DataRow(SINGLE_TYPE, "neq", "real '9.2'", "9.2", "!=")]
+ [DataRow(SINGLE_TYPE, "eq", "real '0.33'", "0.33", "=")]
+ [DataTestMethod]
+ public async Task PG_real_graphql_single_filter_expectedValues(
+ string type,
+ string filterOperator,
+ string sqlValue,
+ string gqlValue,
+ string queryOperator)
+ {
+ await QueryTypeColumnFilterAndOrderBy(type, filterOperator, sqlValue, gqlValue, queryOperator);
+ }
+
+ protected override string MakeQueryOnTypeTable(List queryFields, int id)
{
- return MakeQueryOnTypeTable(columnsToQuery, filterValue: id.ToString(), filterField: "id");
+ return MakeQueryOnTypeTable(queryFields, filterValue: id.ToString(), filterField: "id");
}
protected override string MakeQueryOnTypeTable(
- List queriedColumns,
+ List queryFields,
string filterValue = "1",
string filterOperator = "=",
string filterField = "1",
@@ -42,7 +68,7 @@ protected override string MakeQueryOnTypeTable(
return @"
" + formattedSelect + @"
FROM
- (SELECT " + string.Join(", ", queriedColumns.Select(c => ProperlyFormatTypeTableColumn(c) + $" AS {c}")) + @"
+ (SELECT " + string.Join(", ", queryFields.Select(field => ProperlyFormatTypeTableColumn(field.BackingColumnName) + $" AS {field.Alias}")) + @"
FROM public.type_table AS table0
WHERE " + filterField + " " + filterOperator + " " + filterValue + @"
ORDER BY " + orderBy + @" asc
@@ -60,6 +86,7 @@ protected override bool IsSupportedType(string type)
DATETIME2_TYPE => false,
DATETIMEOFFSET_TYPE => false,
TIME_TYPE => false,
+ LOCALTIME_TYPE => false,
_ => true
};
}