Skip to content

Commit b2f87b5

Browse files
Add tests for graphql
1 parent 35ca2ec commit b2f87b5

1 file changed

Lines changed: 274 additions & 0 deletions

File tree

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
using System.Threading.Tasks;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Azure.DataApiBuilder.Service.Tests.SqlTests.GraphQLQueryTests
9+
{
10+
/// <summary>
11+
/// Tests for SQL Server vector column support via GraphQL (read and write).
12+
/// Verifies that vector columns are exposed as GraphQL lists of Single values, that they can be
13+
/// queried (list and by primary key) and mutated (create/update/delete), and that the values that
14+
/// are read from and written to the <c>vector_type_table</c> round-trip correctly.
15+
/// This complements the REST coverage in
16+
/// <see cref="RestApiTests.MsSqlRestVectorTypesTests"/>.
17+
/// NOTE: The vector data type requires SQL Server 2025 / Azure SQL.
18+
/// </summary>
19+
[TestClass, TestCategory(TestCategory.MSSQL)]
20+
public class MsSqlGraphQLVectorTypesTests : SqlTestBase
21+
{
22+
/// <summary>
23+
/// Tolerance used when comparing the single-precision components of a vector,
24+
/// since vector(N) stores 32-bit floats which may not round-trip exactly through JSON.
25+
/// </summary>
26+
private const double VECTOR_COMPONENT_DELTA = 0.0001;
27+
28+
[ClassInitialize]
29+
public static async Task SetupAsync(TestContext context)
30+
{
31+
DatabaseEngine = TestCategory.MSSQL;
32+
await InitializeTestFixture();
33+
}
34+
35+
#region Read Tests
36+
37+
/// <summary>
38+
/// Query the vector column by primary key and verify the returned list matches the values seeded
39+
/// in the database.
40+
/// </summary>
41+
[DataTestMethod]
42+
[DataRow(1, new[] { 0.5f, 0.25f, 0.75f }, DisplayName = "Query vector data type by primary key")]
43+
[DataRow(2, new[] { 1.5f, -2.5f, 3.5f }, DisplayName = "Query vector data type with negative components")]
44+
[DataRow(3, null, DisplayName = "Query vector data type with null vector")]
45+
public async Task QueryVectorTypeByPk(int id, float[] expectedValues)
46+
{
47+
string graphQLQueryName = "vectorType_by_pk";
48+
string graphQLQuery = @"{
49+
vectorType_by_pk(id: " + id + @") {
50+
id
51+
vector_data
52+
}
53+
}";
54+
55+
JsonElement result = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false);
56+
57+
Assert.AreEqual(id, result.GetProperty("id").GetInt32());
58+
AssertVectorEquals(result.GetProperty("vector_data"), expectedValues);
59+
}
60+
61+
/// <summary>
62+
/// Query the list of vector records and verify the first record (ordered by primary key) matches
63+
/// the seeded value.
64+
/// </summary>
65+
[TestMethod]
66+
public async Task QueryVectorTypeList()
67+
{
68+
string graphQLQueryName = "vectorTypes";
69+
string graphQLQuery = @"{
70+
vectorTypes(orderBy: { id: ASC }) {
71+
items {
72+
id
73+
vector_data
74+
}
75+
}
76+
}";
77+
78+
JsonElement result = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false);
79+
80+
JsonElement items = result.GetProperty("items");
81+
Assert.IsTrue(items.GetArrayLength() >= 1, "Expected at least one vector record.");
82+
83+
JsonElement first = items[0];
84+
Assert.AreEqual(1, first.GetProperty("id").GetInt32());
85+
AssertVectorEquals(first.GetProperty("vector_data"), new[] { 0.5f, 0.25f, 0.75f });
86+
}
87+
88+
/// <summary>
89+
/// Query the maximum-dimension vector column and verify the returned list has the expected number
90+
/// of components and the correct values.
91+
/// </summary>
92+
[TestMethod]
93+
public async Task QueryVectorTypeWithMaxDimensions()
94+
{
95+
string graphQLQueryName = "vectorType_by_pk";
96+
string graphQLQuery = @"{
97+
vectorType_by_pk(id: 7) {
98+
id
99+
vector_data_max
100+
}
101+
}";
102+
103+
JsonElement result = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false);
104+
105+
Assert.AreEqual(7, result.GetProperty("id").GetInt32());
106+
107+
JsonElement maxVector = result.GetProperty("vector_data_max");
108+
Assert.AreEqual(JsonValueKind.Array, maxVector.ValueKind, "Expected the maximum-dimension vector to be a list.");
109+
Assert.AreEqual(1998, maxVector.GetArrayLength(), "Expected the maximum-dimension vector to have 1998 components.");
110+
111+
int i = 1;
112+
foreach (JsonElement vectorVal in maxVector.EnumerateArray())
113+
{
114+
Assert.AreEqual(i, vectorVal.GetDouble(), VECTOR_COMPONENT_DELTA);
115+
i++;
116+
}
117+
}
118+
119+
#endregion
120+
121+
#region Write Tests
122+
123+
/// <summary>
124+
/// Insert a new record with a vector value via a create mutation and verify the persisted value is
125+
/// returned as a list and can be read back correctly. The record is deleted afterwards to keep the
126+
/// table clean.
127+
/// </summary>
128+
[DataTestMethod]
129+
[DataRow("[0.125, 0.25, 0.5]", new[] { 0.125f, 0.25f, 0.5f }, DisplayName = "Insert valid vector")]
130+
[DataRow("null", null, DisplayName = "Insert valid null vector")]
131+
[DataRow("[5e-1, 2.5e-1, 7.5e-1]", new[] { 0.5f, 0.25f, 0.75f }, DisplayName = "Insert valid vector with scientific notation")]
132+
public async Task InsertVectorType(string vectorLiteral, float[] expectedValue)
133+
{
134+
string createMutationName = "createVectorType";
135+
string createMutation = @"mutation {
136+
createVectorType(item: { vector_data: " + vectorLiteral + @" }) {
137+
id
138+
vector_data
139+
}
140+
}";
141+
142+
JsonElement created = await ExecuteGraphQLRequestAsync(createMutation, createMutationName, isAuthenticated: false);
143+
144+
int newId = created.GetProperty("id").GetInt32();
145+
AssertVectorEquals(created.GetProperty("vector_data"), expectedValue);
146+
147+
// Confirm the value was persisted by reading it back.
148+
JsonElement readBack = await GetRecordByPkAsync(newId);
149+
AssertVectorEquals(readBack.GetProperty("vector_data"), expectedValue);
150+
151+
await DeleteVectorTypeAsync(newId);
152+
}
153+
154+
/// <summary>
155+
/// Insert an invalid vector (too many dimensions) via a create mutation and verify the mutation
156+
/// fails with a GraphQL error rather than persisting bad data.
157+
/// </summary>
158+
[TestMethod]
159+
public async Task InsertInvalidVectorTypeFails()
160+
{
161+
string createMutationName = "createVectorType";
162+
string createMutation = @"mutation {
163+
createVectorType(item: { vector_data: [1.25, 2.25, 3.25, 4.25] }) {
164+
id
165+
vector_data
166+
}
167+
}";
168+
169+
JsonElement result = await ExecuteGraphQLRequestAsync(createMutation, createMutationName, isAuthenticated: false, expectsError: true);
170+
171+
SqlTestHelper.TestForErrorInGraphQLResponse(result.ToString());
172+
}
173+
174+
/// <summary>
175+
/// Update an existing record's vector value via an update mutation and verify the new value is
176+
/// persisted, then restore the original value.
177+
/// </summary>
178+
[TestMethod]
179+
public async Task UpdateVectorType()
180+
{
181+
string updateMutationName = "updateVectorType";
182+
183+
// Change vector value.
184+
float[] expected = new[] { 9.5f, 8.5f, 7.5f };
185+
string updateMutation = @"mutation {
186+
updateVectorType(id: 4, item: { vector_data: [9.5, 8.5, 7.5] }) {
187+
id
188+
vector_data
189+
}
190+
}";
191+
192+
JsonElement updated = await ExecuteGraphQLRequestAsync(updateMutation, updateMutationName, isAuthenticated: false);
193+
Assert.AreEqual(4, updated.GetProperty("id").GetInt32());
194+
AssertVectorEquals(updated.GetProperty("vector_data"), expected);
195+
196+
JsonElement readBack = await GetRecordByPkAsync(4);
197+
AssertVectorEquals(readBack.GetProperty("vector_data"), expected);
198+
199+
// Restore vector value to original.
200+
float[] original = new[] { 1.0f, 2.0f, 3.0f };
201+
string restoreMutation = @"mutation {
202+
updateVectorType(id: 4, item: { vector_data: [1.0, 2.0, 3.0] }) {
203+
id
204+
vector_data
205+
}
206+
}";
207+
208+
JsonElement restored = await ExecuteGraphQLRequestAsync(restoreMutation, updateMutationName, isAuthenticated: false);
209+
AssertVectorEquals(restored.GetProperty("vector_data"), original);
210+
}
211+
212+
#endregion
213+
214+
#region Helpers
215+
216+
/// <summary>
217+
/// Fetches a single VectorType record by its primary key via GraphQL and returns the record element.
218+
/// </summary>
219+
private async Task<JsonElement> GetRecordByPkAsync(int id)
220+
{
221+
string graphQLQueryName = "vectorType_by_pk";
222+
string graphQLQuery = @"{
223+
vectorType_by_pk(id: " + id + @") {
224+
id
225+
vector_data
226+
}
227+
}";
228+
229+
JsonElement result = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false);
230+
return result.Clone();
231+
}
232+
233+
/// <summary>
234+
/// Deletes a VectorType record by its primary key via a delete mutation.
235+
/// </summary>
236+
private async Task DeleteVectorTypeAsync(int id)
237+
{
238+
string deleteMutationName = "deleteVectorType";
239+
string deleteMutation = @"mutation {
240+
deleteVectorType(id: " + id + @") {
241+
id
242+
}
243+
}";
244+
245+
JsonElement result = await ExecuteGraphQLRequestAsync(deleteMutation, deleteMutationName, isAuthenticated: false);
246+
Assert.AreEqual(id, result.GetProperty("id").GetInt32());
247+
}
248+
249+
/// <summary>
250+
/// Asserts that the given JSON element is a list whose components match the expected vector within
251+
/// <see cref="VECTOR_COMPONENT_DELTA"/>.
252+
/// </summary>
253+
private static void AssertVectorEquals(JsonElement actual, float[] expected)
254+
{
255+
if (expected == null)
256+
{
257+
Assert.AreEqual(JsonValueKind.Null, actual.ValueKind, "Expected a null vector, but got a non-null value.");
258+
return;
259+
}
260+
261+
Assert.AreEqual(JsonValueKind.Array, actual.ValueKind, "Expected the vector to be serialized as a list.");
262+
Assert.AreEqual(expected.Length, actual.GetArrayLength(), "Vector dimension mismatch.");
263+
264+
int i = 0;
265+
foreach (JsonElement element in actual.EnumerateArray())
266+
{
267+
Assert.AreEqual(expected[i], element.GetDouble(), VECTOR_COMPONENT_DELTA, $"Vector component mismatch at index {i}.");
268+
i++;
269+
}
270+
}
271+
272+
#endregion
273+
}
274+
}

0 commit comments

Comments
 (0)