From 7c25d802bae64aa980292de8bdf69407ff6218a3 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 14 Jul 2026 18:42:54 +0530 Subject: [PATCH 1/6] test(mssql-json): add profiles fixture + REST CRUD tests (Phase 3) Mirrors the merged vector-type feature (#3677). CI now runs SQL Server 2025 (#3697), so the native json column is created unconditionally - no server-version gating needed. - profiles table (id, metadata json) + 5 seed rows (simple/array/nested/unicode/null) (T002) - Profile entity in dab-config.MsSql.json + config-generator command, REST + GraphQL enabled, anon/authenticated CRUD (T003) - MsSqlRestJsonTypesTests: GET list/by-pk/null/array/nested/unicode, POST, PUT, PATCH, PATCH-to-null, DELETE (T011,T013,T015,T017,T019) --- config-generators/mssql-commands.txt | 2 + src/Service.Tests/DatabaseSchema-MsSql.sql | 16 ++ .../RestApiTests/MsSqlRestJsonTypesTests.cs | 241 ++++++++++++++++++ src/Service.Tests/dab-config.MsSql.json | 55 ++++ 4 files changed, 314 insertions(+) create mode 100644 src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs diff --git a/config-generators/mssql-commands.txt b/config-generators/mssql-commands.txt index 99e138b846..df03b27a31 100644 --- a/config-generators/mssql-commands.txt +++ b/config-generators/mssql-commands.txt @@ -18,6 +18,8 @@ add WebsiteUser_MM --config "dab-config.MsSql.json" --source website_users_mm -- add SupportedType --config "dab-config.MsSql.json" --source type_table --permissions "anonymous:create,read,delete,update" add VectorType --config "dab-config.MsSql.json" --source vector_type_table --rest true --graphql false --permissions "anonymous:create,read,delete,update" update VectorType --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" +add Profile --config "dab-config.MsSql.json" --source profiles --rest true --graphql true --permissions "anonymous:create,read,delete,update" +update Profile --config "dab-config.MsSql.json" --permissions "authenticated:create,read,delete,update" add stocks_price --config "dab-config.MsSql.json" --source stocks_price --permissions "authenticated:create,read,update,delete" update stocks_price --config "dab-config.MsSql.json" --permissions "anonymous:read" update stocks_price --config "dab-config.MsSql.json" --permissions "TestNestedFilterFieldIsNull_ColumnForbidden:read" --fields.exclude "price" diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 39c796e3dc..f2c62d1845 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -41,6 +41,7 @@ DROP TABLE IF EXISTS comics; DROP TABLE IF EXISTS brokers; DROP TABLE IF EXISTS type_table; DROP TABLE IF EXISTS vector_type_table; +DROP TABLE IF EXISTS profiles; DROP TABLE IF EXISTS trees; DROP TABLE IF EXISTS fungi; DROP TABLE IF EXISTS empty_table; @@ -241,6 +242,11 @@ CREATE TABLE vector_type_table( vector_data_max vector(1998) ); +CREATE TABLE profiles( + id int IDENTITY(1, 1) PRIMARY KEY, + metadata json NULL +); + CREATE TABLE trees ( treeId int PRIMARY KEY, species varchar(max), @@ -638,6 +644,16 @@ VALUES (7, CAST('[' + ( SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), ',') WITHIN GROUP (ORDER BY value) FROM GENERATE_SERIES(1, 1998) ) + ']' AS vector(1998))); + +SET IDENTITY_INSERT profiles ON +INSERT INTO profiles(id, metadata) +VALUES + (1, N'{"role":"admin","tier":3}'), + (2, N'{"tags":["a","b","c"]}'), + (3, N'{"nested":{"key":{"deep":true}}}'), + (4, N'{"unicode":"éü😀"}'), + (5, NULL); +SET IDENTITY_INSERT profiles OFF SET IDENTITY_INSERT vector_type_table OFF SET IDENTITY_INSERT sales ON diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs new file mode 100644 index 0000000000..157611dc7b --- /dev/null +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.SqlTests.RestApiTests +{ + /// + /// Tests for SQL Server native JSON column support via REST endpoints (read and write). + /// DAB treats a JSON column exactly like a string column: the raw JSON text round-trips + /// through create/read/update/delete without any special handling, new scalar, or format. + /// Assertions parse the returned metadata semantically so they are robust to any + /// whitespace / key-order normalization the engine applies to the JSON type. + /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. + /// + [TestClass, TestCategory(TestCategory.MSSQL)] + public class MsSqlRestJsonTypesTests : SqlTestBase + { + private const string JSON_TYPE_REST_PATH = "api/Profile"; + + [ClassInitialize] + public static async Task SetupAsync(TestContext context) + { + DatabaseEngine = TestCategory.MSSQL; + await InitializeTestFixture(); + } + + #region Read Tests + + /// + /// GET /api/Profile - Verify the whole collection (5 seeded rows) is returned and that + /// each metadata value renders either as its JSON payload or null (row 5). + /// + [TestMethod] + public async Task GetJsonTypeList() + { + HttpResponseMessage response = await HttpClient.GetAsync(JSON_TYPE_REST_PATH); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + JsonElement items = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value"); + Assert.AreEqual(5, items.GetArrayLength(), "Expected the 5 seeded profile rows."); + } + + /// + /// GET /api/Profile/id/1 - Verify a simple object payload round-trips (verbatim value-equivalence). + /// + [TestMethod] + public async Task GetJsonTypeByPrimaryKey() + { + JsonElement metadata = ParseMetadata(await GetRecordByIdAsync(1)); + Assert.AreEqual("admin", metadata.GetProperty("role").GetString()); + Assert.AreEqual(3, metadata.GetProperty("tier").GetInt32()); + } + + /// + /// GET /api/Profile/id/5 - Verify a SQL NULL metadata value is rendered as JSON null. + /// + [TestMethod] + public async Task GetJsonTypeWithNull() + { + JsonElement record = await GetRecordByIdAsync(5); + Assert.AreEqual(JsonValueKind.Null, record.GetProperty("metadata").ValueKind); + } + + /// + /// GET /api/Profile/id/2 - Verify an array-bearing payload is preserved. + /// + [TestMethod] + public async Task GetJsonTypeWithArrayPayload() + { + JsonElement metadata = ParseMetadata(await GetRecordByIdAsync(2)); + JsonElement tags = metadata.GetProperty("tags"); + Assert.AreEqual(JsonValueKind.Array, tags.ValueKind); + Assert.AreEqual(3, tags.GetArrayLength()); + Assert.AreEqual("a", tags[0].GetString()); + Assert.AreEqual("b", tags[1].GetString()); + Assert.AreEqual("c", tags[2].GetString()); + } + + /// + /// GET /api/Profile/id/3 - Verify a deeply nested object payload is preserved. + /// + [TestMethod] + public async Task GetJsonTypeWithNestedPayload() + { + JsonElement metadata = ParseMetadata(await GetRecordByIdAsync(3)); + Assert.IsTrue(metadata.GetProperty("nested").GetProperty("key").GetProperty("deep").GetBoolean()); + } + + /// + /// GET /api/Profile/id/4 - Verify unicode (including a multi-byte emoji) round-trips intact. + /// + [TestMethod] + public async Task GetJsonTypeWithUnicode() + { + JsonElement metadata = ParseMetadata(await GetRecordByIdAsync(4)); + Assert.AreEqual("éü😀", metadata.GetProperty("unicode").GetString()); + } + + #endregion + + #region Write Tests + + /// + /// POST /api/Profile - Verify a new record with a JSON payload can be inserted, the value + /// echoes back, and it is persisted (read-back). Also covers inserting a null payload. + /// + [DataTestMethod] + [DataRow("{ \"metadata\": \"{\\\"role\\\":\\\"guest\\\"}\" }", false, DisplayName = "Insert profile with valid JSON object")] + [DataRow("{ \"metadata\": null }", true, DisplayName = "Insert profile with null metadata")] + public async Task InsertJsonType(string requestBody, bool expectNull) + { + HttpResponseMessage postResponse = await HttpClient.PostAsync( + JSON_TYPE_REST_PATH, + new StringContent(requestBody, Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.Created, postResponse.StatusCode); + + JsonElement postElement = JsonDocument.Parse(await postResponse.Content.ReadAsStringAsync()) + .RootElement.GetProperty("value")[0]; + int newId = postElement.GetProperty("id").GetInt32(); + + JsonElement readBack = await GetRecordByIdAsync(newId); + if (expectNull) + { + Assert.AreEqual(JsonValueKind.Null, readBack.GetProperty("metadata").ValueKind); + } + else + { + Assert.AreEqual("guest", ParseMetadata(readBack).GetProperty("role").GetString()); + } + + await DeleteProfile(newId); + } + + /// + /// PUT /api/Profile/id/1 - Verify a full update replaces the metadata payload, then restore it. + /// + [TestMethod] + public async Task PutJsonType_Update() + { + HttpResponseMessage response = await HttpClient.PutAsync( + $"{JSON_TYPE_REST_PATH}/id/1", + new StringContent("{ \"metadata\": \"{\\\"role\\\":\\\"owner\\\"}\" }", Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("owner", ParseMetadata(await GetRecordByIdAsync(1)).GetProperty("role").GetString()); + + // Restore original value. + HttpResponseMessage restore = await HttpClient.PutAsync( + $"{JSON_TYPE_REST_PATH}/id/1", + new StringContent("{ \"metadata\": \"{\\\"role\\\":\\\"admin\\\",\\\"tier\\\":3}\" }", Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, restore.StatusCode); + Assert.AreEqual("admin", ParseMetadata(await GetRecordByIdAsync(1)).GetProperty("role").GetString()); + } + + /// + /// PATCH /api/Profile/id/1 - Verify a partial update sets a new metadata payload, then restore it. + /// + [TestMethod] + public async Task PatchJsonType_Update() + { + HttpResponseMessage response = await HttpClient.PatchAsync( + $"{JSON_TYPE_REST_PATH}/id/1", + new StringContent("{ \"metadata\": \"{\\\"role\\\":\\\"editor\\\"}\" }", Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual("editor", ParseMetadata(await GetRecordByIdAsync(1)).GetProperty("role").GetString()); + + // Restore original value. + HttpResponseMessage restore = await HttpClient.PutAsync( + $"{JSON_TYPE_REST_PATH}/id/1", + new StringContent("{ \"metadata\": \"{\\\"role\\\":\\\"admin\\\",\\\"tier\\\":3}\" }", Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, restore.StatusCode); + } + + /// + /// PATCH /api/Profile/id/1 - Verify metadata can be cleared to null. + /// + [TestMethod] + public async Task PatchJsonType_ToNull() + { + HttpResponseMessage response = await HttpClient.PatchAsync( + $"{JSON_TYPE_REST_PATH}/id/2", + new StringContent("{ \"metadata\": null }", Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + Assert.AreEqual(JsonValueKind.Null, (await GetRecordByIdAsync(2)).GetProperty("metadata").ValueKind); + + // Restore original array payload. + HttpResponseMessage restore = await HttpClient.PutAsync( + $"{JSON_TYPE_REST_PATH}/id/2", + new StringContent("{ \"metadata\": \"{\\\"tags\\\":[\\\"a\\\",\\\"b\\\",\\\"c\\\"]}\" }", Encoding.UTF8, "application/json")); + Assert.AreEqual(HttpStatusCode.OK, restore.StatusCode); + } + + #endregion + + #region Helpers + + /// + /// DELETE /api/Profile/id/{id} - Verify a record can be deleted. + /// + private static async Task DeleteProfile(int id) + { + HttpResponseMessage deleteResponse = await HttpClient.DeleteAsync($"{JSON_TYPE_REST_PATH}/id/{id}"); + Assert.AreEqual(HttpStatusCode.NoContent, deleteResponse.StatusCode); + } + + /// + /// Fetches a single Profile record by its primary key and returns the record element. + /// + private static async Task GetRecordByIdAsync(int id) + { + HttpResponseMessage response = await HttpClient.GetAsync($"{JSON_TYPE_REST_PATH}/id/{id}"); + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + string body = await response.Content.ReadAsStringAsync(); + return JsonDocument.Parse(body).RootElement.GetProperty("value")[0].Clone(); + } + + /// + /// Returns the metadata field parsed as a JSON element. DAB treats a JSON column as a string, + /// so the value is expected to arrive as a JSON string carrying the payload; this helper also + /// tolerates the value arriving as a raw JSON object so the assertions remain robust. + /// + private static JsonElement ParseMetadata(JsonElement record) + { + JsonElement metadata = record.GetProperty("metadata"); + if (metadata.ValueKind == JsonValueKind.String) + { + return JsonDocument.Parse(metadata.GetString()).RootElement.Clone(); + } + + return metadata.Clone(); + } + + #endregion + } +} diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index 4de4f52b5f..a922827e2b 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -1854,6 +1854,61 @@ } ] }, + "Profile": { + "source": { + "object": "profiles", + "type": "table", + "key-fields": [ + "id" + ] + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Profile", + "plural": "Profiles" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "delete" + }, + { + "action": "update" + } + ] + }, + { + "role": "authenticated", + "actions": [ + { + "action": "create" + }, + { + "action": "read" + }, + { + "action": "delete" + }, + { + "action": "update" + } + ] + } + ] + }, "stocks_price": { "source": { "object": "stocks_price", From 712b39774f908b4257a50881d39e6876a9dd74cc Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 14 Jul 2026 20:26:16 +0530 Subject: [PATCH 2/6] fix(mssql-json): close vector_type_table IDENTITY_INSERT before profiles seed The vector_type_table block left IDENTITY_INSERT ON (it was previously the last such block). Inserting the profiles seed after it triggered 'IDENTITY_INSERT is already ON for table vector_type_table', aborting schema init for the whole MsSql suite. Turn vector OFF before the profiles ON/OFF block. --- src/Service.Tests/DatabaseSchema-MsSql.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index f2c62d1845..a100d50c79 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -644,6 +644,7 @@ VALUES (7, CAST('[' + ( SELECT STRING_AGG(CAST(value AS NVARCHAR(MAX)), ',') WITHIN GROUP (ORDER BY value) FROM GENERATE_SERIES(1, 1998) ) + ']' AS vector(1998))); +SET IDENTITY_INSERT vector_type_table OFF SET IDENTITY_INSERT profiles ON INSERT INTO profiles(id, metadata) @@ -654,7 +655,6 @@ VALUES (4, N'{"unicode":"éü😀"}'), (5, NULL); SET IDENTITY_INSERT profiles OFF -SET IDENTITY_INSERT vector_type_table OFF SET IDENTITY_INSERT sales ON INSERT INTO sales(id, item_name, subtotal, tax) VALUES (1, 'Watch', 249.00, 20.59), (2, 'Montior', 120.50, 11.12); From 320bd5656e4fed0cf5a7366c2b4698e87b9a71d7 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Tue, 14 Jul 2026 20:27:51 +0530 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Service.Tests/DatabaseSchema-MsSql.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index a100d50c79..c34f961aab 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -243,7 +243,7 @@ CREATE TABLE vector_type_table( ); CREATE TABLE profiles( - id int IDENTITY(1, 1) PRIMARY KEY, + id int IDENTITY(5001, 1) PRIMARY KEY, metadata json NULL ); From e142cb2bd6caccdf1d0eeb8aeb3fd47e05f31794 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 14 Jul 2026 20:40:17 +0530 Subject: [PATCH 4/6] test(mssql-json): address PR review feedback - Align profiles identity seed to IDENTITY(5001, 1) per the schema convention (line 74) - Tighten ParseMetadata to assert metadata is a JSON string (proves the Phase 2 string-contract), no raw-object fallback - GetJsonTypeList now asserts each row metadata is a JSON string or null - Fix PatchJsonType_ToNull doc comment to reference id/2 --- .../RestApiTests/MsSqlRestJsonTypesTests.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs index 157611dc7b..a37861fea7 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs @@ -34,7 +34,7 @@ public static async Task SetupAsync(TestContext context) /// /// GET /api/Profile - Verify the whole collection (5 seeded rows) is returned and that - /// each metadata value renders either as its JSON payload or null (row 5). + /// each metadata value renders either as a JSON string payload or null (row 5). /// [TestMethod] public async Task GetJsonTypeList() @@ -45,6 +45,15 @@ public async Task GetJsonTypeList() JsonElement items = JsonDocument.Parse(await response.Content.ReadAsStringAsync()) .RootElement.GetProperty("value"); Assert.AreEqual(5, items.GetArrayLength(), "Expected the 5 seeded profile rows."); + + // Rows 1-4 carry a JSON payload (returned as a JSON string); row 5 is null. + foreach (JsonElement record in items.EnumerateArray()) + { + JsonValueKind metadataKind = record.GetProperty("metadata").ValueKind; + Assert.IsTrue( + metadataKind is JsonValueKind.String or JsonValueKind.Null, + $"Expected metadata to be a JSON string or null, but was {metadataKind}."); + } } /// @@ -178,7 +187,7 @@ public async Task PatchJsonType_Update() } /// - /// PATCH /api/Profile/id/1 - Verify metadata can be cleared to null. + /// PATCH /api/Profile/id/2 - Verify metadata can be cleared to null. /// [TestMethod] public async Task PatchJsonType_ToNull() @@ -222,18 +231,18 @@ private static async Task GetRecordByIdAsync(int id) /// /// Returns the metadata field parsed as a JSON element. DAB treats a JSON column as a string, - /// so the value is expected to arrive as a JSON string carrying the payload; this helper also - /// tolerates the value arriving as a raw JSON object so the assertions remain robust. + /// so a non-null metadata value MUST arrive at the REST boundary as a JSON string carrying the + /// payload (the Phase 2 contract). This helper asserts that and parses the string payload. /// private static JsonElement ParseMetadata(JsonElement record) { JsonElement metadata = record.GetProperty("metadata"); - if (metadata.ValueKind == JsonValueKind.String) - { - return JsonDocument.Parse(metadata.GetString()).RootElement.Clone(); - } + Assert.AreEqual( + JsonValueKind.String, + metadata.ValueKind, + "A JSON column must be returned as a JSON string at the REST boundary (treated as a normal string)."); - return metadata.Clone(); + return JsonDocument.Parse(metadata.GetString()).RootElement.Clone(); } #endregion From 70464b97353bdea8b2a5d5abf183022dd003f996 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 15 Jul 2026 09:18:21 +0530 Subject: [PATCH 5/6] test(mssql-json): expect native JSON object on read + refresh config snapshot CI on SQL Server 2025 shows a native json column is inlined by DAB's FOR JSON PATH projection as a nested JSON object (like vector columns become native arrays), not an escaped string. DAB applies no special handling - SQL Server inlines it. Correct the read-side assertions accordingly: - ParseMetadata now asserts metadata.ValueKind == Object and returns it directly (no string parse). - GetJsonTypeList asserts each row metadata is a JSON object or null. - Update doc comments to describe the FOR JSON PATH inlining behavior. Also refresh ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt to include the new Profile entity (only delta). --- ...tReadingRuntimeConfigForMsSql.verified.txt | 55 +++++++++++++++++++ .../RestApiTests/MsSqlRestJsonTypesTests.cs | 30 +++++----- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 6e43d2599a..2049febe1e 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -1765,6 +1765,61 @@ ] } }, + { + Profile: { + Source: { + Object: profiles, + Type: Table, + KeyFields: [ + id + ] + }, + GraphQL: { + Singular: Profile, + Plural: Profiles, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Create + }, + { + Action: Read + }, + { + Action: Delete + }, + { + Action: Update + } + ] + }, + { + Role: authenticated, + Actions: [ + { + Action: Create + }, + { + Action: Read + }, + { + Action: Delete + }, + { + Action: Update + } + ] + } + ] + } + }, { stocks_price: { Source: { diff --git a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs index a37861fea7..7a5a41734d 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/MsSqlRestJsonTypesTests.cs @@ -12,9 +12,13 @@ namespace Azure.DataApiBuilder.Service.Tests.SqlTests.RestApiTests { /// /// Tests for SQL Server native JSON column support via REST endpoints (read and write). - /// DAB treats a JSON column exactly like a string column: the raw JSON text round-trips - /// through create/read/update/delete without any special handling, new scalar, or format. - /// Assertions parse the returned metadata semantically so they are robust to any + /// DAB does nothing special for a JSON column - it is written from the request payload and + /// read back with no custom handling, new scalar, or format. On read, SQL Server's + /// FOR JSON PATH projection (which DAB uses to shape every result) inlines a native json + /// column as a nested JSON value, so the metadata surfaces at the REST boundary as a JSON + /// object rather than an escaped string. Writes still supply the payload as a JSON string, + /// exactly as a normal string column would be written. + /// Assertions compare the returned metadata semantically so they are robust to any /// whitespace / key-order normalization the engine applies to the JSON type. /// NOTE: The native JSON data type requires SQL Server 2025 / Azure SQL. /// @@ -34,7 +38,7 @@ public static async Task SetupAsync(TestContext context) /// /// GET /api/Profile - Verify the whole collection (5 seeded rows) is returned and that - /// each metadata value renders either as a JSON string payload or null (row 5). + /// each metadata value renders either as a native JSON object payload or null (row 5). /// [TestMethod] public async Task GetJsonTypeList() @@ -46,13 +50,13 @@ public async Task GetJsonTypeList() .RootElement.GetProperty("value"); Assert.AreEqual(5, items.GetArrayLength(), "Expected the 5 seeded profile rows."); - // Rows 1-4 carry a JSON payload (returned as a JSON string); row 5 is null. + // Rows 1-4 carry a JSON payload (inlined as a native JSON object); row 5 is null. foreach (JsonElement record in items.EnumerateArray()) { JsonValueKind metadataKind = record.GetProperty("metadata").ValueKind; Assert.IsTrue( - metadataKind is JsonValueKind.String or JsonValueKind.Null, - $"Expected metadata to be a JSON string or null, but was {metadataKind}."); + metadataKind is JsonValueKind.Object or JsonValueKind.Null, + $"Expected metadata to be a JSON object or null, but was {metadataKind}."); } } @@ -230,19 +234,19 @@ private static async Task GetRecordByIdAsync(int id) } /// - /// Returns the metadata field parsed as a JSON element. DAB treats a JSON column as a string, - /// so a non-null metadata value MUST arrive at the REST boundary as a JSON string carrying the - /// payload (the Phase 2 contract). This helper asserts that and parses the string payload. + /// Returns the metadata field as a JSON element. DAB applies no special handling to a JSON + /// column, so SQL Server's FOR JSON PATH projection inlines it as a native JSON object at the + /// REST boundary. This helper asserts the value is a JSON object and returns it for inspection. /// private static JsonElement ParseMetadata(JsonElement record) { JsonElement metadata = record.GetProperty("metadata"); Assert.AreEqual( - JsonValueKind.String, + JsonValueKind.Object, metadata.ValueKind, - "A JSON column must be returned as a JSON string at the REST boundary (treated as a normal string)."); + "A native JSON column is inlined as a JSON object at the REST boundary via FOR JSON PATH."); - return JsonDocument.Parse(metadata.GetString()).RootElement.Clone(); + return metadata.Clone(); } #endregion From ade13419de5850bcdd39eac05476565761c8ce9e Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 15 Jul 2026 10:48:43 +0530 Subject: [PATCH 6/6] test(mssql-json): match Profile entity to config-generator output (drop key-fields) CI regenerates dab-config.MsSql.json from config-generators/mssql-commands.txt before running tests. The 'add Profile' command specifies no --source.key-fields (profiles is a table, so the PK is inferred from the DB schema, consistent with VectorType and every other table entity). The hand-added key-fields made the committed config - and the verified snapshot - diverge from the generated config, so TestReadingRuntimeConfigForMsSql failed on CI (Source.KeyFields present in verified, absent in the generated config it deserializes). Remove key-fields from the Profile source and refresh the snapshot to match. --- ...rationTests.TestReadingRuntimeConfigForMsSql.verified.txt | 5 +---- src/Service.Tests/dab-config.MsSql.json | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 2049febe1e..2482d40c29 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -1769,10 +1769,7 @@ Profile: { Source: { Object: profiles, - Type: Table, - KeyFields: [ - id - ] + Type: Table }, GraphQL: { Singular: Profile, diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index a922827e2b..6e1beafefa 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -1857,10 +1857,7 @@ "Profile": { "source": { "object": "profiles", - "type": "table", - "key-fields": [ - "id" - ] + "type": "table" }, "graphql": { "enabled": true,