From 626d6f04562a3a960132ecc847e5df03f9c7c58b Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sat, 4 Jul 2026 19:37:38 -0700 Subject: [PATCH 01/11] Build upsert queries appropriately with insert/create policies --- src/Core/Resolvers/PostgresQueryBuilder.cs | 42 +++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Core/Resolvers/PostgresQueryBuilder.cs b/src/Core/Resolvers/PostgresQueryBuilder.cs index 244b1a45b8..da738f37d0 100644 --- a/src/Core/Resolvers/PostgresQueryBuilder.cs +++ b/src/Core/Resolvers/PostgresQueryBuilder.cs @@ -17,6 +17,7 @@ public class PostgresQueryBuilder : BaseSqlQueryBuilder, IQueryBuilder private const string UPSERT_IDENTIFIER_COLUMN_NAME = "___upsert_op___"; private const string INSERT_UPSERT = "inserted"; private const string UPDATE_UPSERT = "updated"; + public const string COUNT_ROWS_WITH_GIVEN_PK = "cnt_rows_to_update"; private static DbCommandBuilder _builder = new NpgsqlCommandBuilder(); @@ -117,25 +118,50 @@ public string Build(SqlUpsertQueryStructure structure) { // https://stackoverflow.com/questions/42668720/check-if-postgres-query-inserted-or-updated-via-upsert // relying on xmax to detect insert vs update breaks for views - string updatePredicates = JoinPredicateStrings(Build(structure.Predicates), structure.GetDbPolicyForOperation(EntityActionOperation.Update)); - string updateQuery = $"UPDATE {QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} " + + string tableName = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)}"; + string pkPredicates = Build(structure.Predicates); + + // RS1: COUNT of rows matching PK (no policy) — used to distinguish + // "row doesn't exist" from "row exists but policy blocked" in the executor. + string countQuery = $"SELECT COUNT(*) AS {COUNT_ROWS_WITH_GIVEN_PK} FROM {tableName} WHERE {pkPredicates}"; + + string updatePredicates = JoinPredicateStrings(pkPredicates, structure.GetDbPolicyForOperation(EntityActionOperation.Update)); + string updateQuery = $"UPDATE {tableName} " + $"SET {Build(structure.UpdateOperations, ", ")} " + $"WHERE {updatePredicates} " + $"RETURNING {Build(structure.OutputColumns)}, '{UPDATE_UPSERT}' AS {UPSERT_IDENTIFIER_COLUMN_NAME}"; if (structure.IsFallbackToUpdate) { - return updateQuery + ";"; + // RS2: UPDATE only — no INSERT branch for autogen PK or missing required columns. + return $"{countQuery}; {updateQuery};"; } else { - return $"WITH update_cte AS ( {updateQuery} ), insert_cte AS ( " + - $"INSERT INTO {QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} ({Build(structure.InsertColumns)}) " + - $"SELECT {string.Join(", ", (structure.Values))} " + - $"WHERE NOT EXISTS (SELECT 1 FROM update_cte) " + + // INSERT only runs when row doesn't exist (pkPredicates match nothing) + // AND the create policy (if any) is satisfied. + string insertPredicates = JoinPredicateStrings( + $"NOT EXISTS (SELECT 1 FROM {tableName} WHERE {pkPredicates})", + structure.GetDbPolicyForOperation(EntityActionOperation.Create)); + + // Alias each value with its column name so that policy predicates referencing + // column names (e.g. "pieceid" != @param) can be resolved in the WHERE clause. + // Using SELECT ... FROM (SELECT @p1 AS col1, ...) AS T avoids both the VALUES(NULL) + // type inference issue and the unnamed-column resolution issue. + string namedValues = string.Join(", ", + structure.InsertColumns.Zip(structure.Values, + (col, val) => $"{val} AS {QuoteIdentifier(col)}")); + + // RS2: CTE that attempts UPDATE first; falls through to INSERT only when row is absent. + string cteQuery = $"WITH update_cte AS ( {updateQuery} ), insert_cte AS ( " + + $"INSERT INTO {tableName} ({Build(structure.InsertColumns)}) " + + $"SELECT {Build(structure.InsertColumns)} FROM (SELECT {namedValues}) AS T " + + $"WHERE {insertPredicates} " + $"RETURNING {Build(structure.OutputColumns)}, '{INSERT_UPSERT}' AS {UPSERT_IDENTIFIER_COLUMN_NAME} ) " + - $"SELECT {BuildListOfLabels(structure.OutputColumns)}, {UPSERT_IDENTIFIER_COLUMN_NAME} FROM update_cte UNION " + + $"SELECT {BuildListOfLabels(structure.OutputColumns)}, {UPSERT_IDENTIFIER_COLUMN_NAME} FROM update_cte UNION ALL " + $"SELECT {BuildListOfLabels(structure.OutputColumns)}, {UPSERT_IDENTIFIER_COLUMN_NAME} FROM insert_cte;"; + + return $"{countQuery}; {cteQuery}"; } } From 3c9b908055b915c6fc03e7ee14b45e929bef0eb0 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sat, 4 Jul 2026 19:39:36 -0700 Subject: [PATCH 02/11] Change logic to determine how to process result sets so that appropriate error/success message is returned --- src/Core/Resolvers/PostgreSqlExecutor.cs | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/Core/Resolvers/PostgreSqlExecutor.cs b/src/Core/Resolvers/PostgreSqlExecutor.cs index 70fa0f1079..f9cf789c1c 100644 --- a/src/Core/Resolvers/PostgreSqlExecutor.cs +++ b/src/Core/Resolvers/PostgreSqlExecutor.cs @@ -2,11 +2,13 @@ // Licensed under the MIT License. using System.Data.Common; +using System.Net; using Azure.Core; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Service.Exceptions; using Azure.Identity; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -146,6 +148,89 @@ private static bool ShouldManagedIdentityAccessBeAttempted(NpgsqlConnectionStrin return string.IsNullOrEmpty(builder.Password); } + /// + public override async Task GetMultipleResultSetsIfAnyAsync( + DbDataReader dbDataReader, List? args = null) + { + // RS1: COUNT of rows matching PK (no policy) — used to distinguish + // "row doesn't exist" from "row exists but policy blocked". + DbResultSet resultSetWithCountOfRowsWithGivenPk = await ExtractResultSetFromDbDataReaderAsync(dbDataReader); + DbResultSetRow? resultSetRowWithCountOfRowsWithGivenPk = resultSetWithCountOfRowsWithGivenPk.Rows.FirstOrDefault(); + int numOfRecordsWithGivenPK; + + if (resultSetRowWithCountOfRowsWithGivenPk is not null && + resultSetRowWithCountOfRowsWithGivenPk.Columns.TryGetValue(PostgresQueryBuilder.COUNT_ROWS_WITH_GIVEN_PK, out object? rowsWithGivenPK)) + { + // PostgreSQL COUNT(*) returns Int64; convert to int. + numOfRecordsWithGivenPK = Convert.ToInt32(rowsWithGivenPK!); + } + else + { + throw new DataApiBuilderException( + message: $"Neither insert nor update could be performed.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + // RS2: UPDATE result, or UPDATE+INSERT CTE result. + DbResultSet dbResultSet = await dbDataReader.NextResultAsync() + ? await ExtractResultSetFromDbDataReaderAsync(dbDataReader) + : throw new DataApiBuilderException( + message: $"Neither insert nor update could be performed.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + + if (numOfRecordsWithGivenPK == 1) // Row existed — we attempted an UPDATE. + { + if (dbResultSet.Rows.Count == 0) + { + // Row exists but UPDATE returned no rows — update policy blocked it. + throw new DataApiBuilderException( + message: DataApiBuilderException.AUTHORIZATION_FAILURE, + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + } + + dbResultSet.ResultProperties.Add(SqlMutationEngine.IS_UPDATE_RESULT_SET, true); + } + else if (dbResultSet.Rows.Count == 0) + { + // Check whether IsFallbackToUpdate was set (passed as args[2]). + // If true, the row simply didn't exist — return 404 (same as MsSql's null-RS2 path). + // If false (or not present), the INSERT ran but create policy blocked it — return 403. + bool isFallbackToUpdate = args is not null && args.Count > 2 + && bool.TryParse(args[2], out bool fallback) && fallback; + + if (isFallbackToUpdate) + { + if (args is not null && args.Count > 1) + { + string prettyPrintPk = args[0]; + string entityName = args[1]; + + throw new DataApiBuilderException( + message: $"Cannot perform INSERT and could not find {entityName} " + + $"with primary key {prettyPrintPk} to perform UPDATE on.", + statusCode: HttpStatusCode.NotFound, + subStatusCode: DataApiBuilderException.SubStatusCodes.ItemNotFound); + } + + throw new DataApiBuilderException( + message: $"Neither insert nor update could be performed.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + // Row didn't exist but INSERT returned no rows — create policy blocked it. + throw new DataApiBuilderException( + message: DataApiBuilderException.AUTHORIZATION_FAILURE, + statusCode: HttpStatusCode.Forbidden, + subStatusCode: DataApiBuilderException.SubStatusCodes.DatabasePolicyFailure); + } + + return dbResultSet; + } + /// /// Determines if the saved default azure credential's access token is valid and not expired. /// From 73d536c78b86d4ec9997a43e9c43b4f53a52e83d Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sat, 4 Jul 2026 19:42:02 -0700 Subject: [PATCH 03/11] Provide another parameter when executing the query to help with result processing --- src/Core/Resolvers/SqlMutationEngine.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index 204d71d44f..dfa032344f 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -2015,6 +2015,7 @@ private async Task IQueryBuilder queryBuilder = _queryManagerFactory.GetQueryBuilder(sqlMetadataProvider.GetDatabaseType()); IQueryExecutor queryExecutor = _queryManagerFactory.GetQueryExecutor(sqlMetadataProvider.GetDatabaseType()); string dataSourceName = _runtimeConfigProvider.GetConfig().GetDataSourceNameFromEntityName(entityName); + bool isFallbackToUpdate; if (operationType is EntityActionOperation.Upsert) { @@ -2028,6 +2029,7 @@ private async Task incrementalUpdate: false); queryString = queryBuilder.Build(upsertStructure); queryParameters = upsertStructure.Parameters; + isFallbackToUpdate = upsertStructure.IsFallbackToUpdate; } else { @@ -2041,6 +2043,7 @@ private async Task incrementalUpdate: true); queryString = queryBuilder.Build(upsertIncrementalStructure); queryParameters = upsertIncrementalStructure.Parameters; + isFallbackToUpdate = upsertIncrementalStructure.IsFallbackToUpdate; } string prettyPrintPk = "<" + string.Join(", ", context.PrimaryKeyValuePairs.Select( @@ -2053,7 +2056,7 @@ private async Task queryExecutor.GetMultipleResultSetsIfAnyAsync, dataSourceName, GetHttpContext(), - new List { prettyPrintPk, entityName }); + new List { prettyPrintPk, entityName, isFallbackToUpdate.ToString() }); } private Dictionary PrepareParameters(RestRequestContext context) From b5f37341522818abaf83d736b84118fc0fc72482 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sat, 4 Jul 2026 19:45:31 -0700 Subject: [PATCH 04/11] Add (unignore) relevant tests --- .../Patch/PostgreSqlPatchApiTests.cs | 34 +++++++------------ .../RestApiTests/Put/PostgreSqlPutApiTests.cs | 34 +++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs index b80658df83..e13c0211b3 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs @@ -166,6 +166,19 @@ SELECT to_jsonb(subq) AS data ) AS subq " }, + { + "PatchOneInsertWithDatabasePolicy", + @" + SELECT to_jsonb(subq) AS data + FROM ( + SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" + FROM " + _Composite_NonAutoGenPK_TableName + @" + WHERE categoryid = 0 AND pieceid = 7 AND ""categoryName"" = 'SciFi' + AND ""piecesAvailable"" = 4 AND ""piecesRequired"" = 0 + AND (pieceid != 6 AND ""piecesAvailable"" > 0) + ) AS subq + " + }, { "PatchOne_Update_Default_Test", @" @@ -335,27 +348,6 @@ await base.PatchOneViewBadRequestTest( } #region overridden tests - - [TestMethod] - [Ignore] - public override Task PatchOneUpdateWithUnsatisfiedDatabasePolicy() - { - throw new NotImplementedException(); - } - - [TestMethod] - [Ignore] - public override Task PatchOneInsertWithUnsatisfiedDatabasePolicy() - { - throw new NotImplementedException(); - } - - [TestMethod] - [Ignore] - public override Task PatchOneInsertWithDatabasePolicy() - { - throw new NotImplementedException(); - } #endregion #region Test Fixture Setup diff --git a/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs index 1e2b028665..3eed9e3be5 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs @@ -85,6 +85,19 @@ SELECT to_jsonb(subq) AS data ) AS subq " }, + { + "PutOneInsertWithDatabasePolicy", + @" + SELECT to_jsonb(subq) AS data + FROM ( + SELECT categoryid, pieceid, ""categoryName"", ""piecesAvailable"", ""piecesRequired"" + FROM " + _Composite_NonAutoGenPK_TableName + @" + WHERE categoryid = 0 AND pieceid = 7 AND ""categoryName"" = 'SciFi' + AND ""piecesAvailable"" = 4 AND ""piecesRequired"" = 0 + AND (pieceid != 6 AND ""piecesAvailable"" > 0) + ) AS subq + " + }, { "PutOneUpdateAccessibleRowWithDatabasePolicy", @" @@ -434,27 +447,6 @@ public static async Task SetupAsync(TestContext context) #endregion #region overridden tests - - [TestMethod] - [Ignore] - public override Task PutOneInsertWithDatabasePolicy() - { - throw new NotImplementedException(); - } - - [TestMethod] - [Ignore] - public override Task PutOneWithUnsatisfiedDatabasePolicy() - { - throw new NotImplementedException(); - } - - [TestMethod] - [Ignore] - public override Task PutOneInsertInTableWithFieldsInDbPolicyNotPresentInBody() - { - throw new NotImplementedException(); - } #endregion [TestCleanup] From 000ec1b6e7879d1e621238084963d35ca9034637 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sat, 4 Jul 2026 19:47:18 -0700 Subject: [PATCH 05/11] Add create policy config and permissions for PostgreSQL tests --- src/Core/Configurations/RuntimeConfigValidator.cs | 3 ++- src/Service.Tests/dab-config.PostgreSql.json | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index 0672eebc8f..481dcef5e8 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -46,7 +46,8 @@ public class RuntimeConfigValidator : IConfigValidator private static readonly HashSet _databaseTypesSupportingCreatePolicy = [ DatabaseType.MSSQL, - DatabaseType.DWSQL + DatabaseType.DWSQL, + DatabaseType.PostgreSQL ]; // Error messages for user-delegated authentication configuration. diff --git a/src/Service.Tests/dab-config.PostgreSql.json b/src/Service.Tests/dab-config.PostgreSql.json index 48f9700754..b9bfae0ec3 100644 --- a/src/Service.Tests/dab-config.PostgreSql.json +++ b/src/Service.Tests/dab-config.PostgreSql.json @@ -324,7 +324,10 @@ } }, { - "action": "create" + "action": "create", + "policy": { + "database": "@item.pieceid ne 6 and @item.piecesAvailable gt 0" + } }, { "action": "read" From 4e06b23ad137c86e8d69cf1b5d96da1fdbc6a2d2 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sat, 4 Jul 2026 19:58:43 -0700 Subject: [PATCH 06/11] Modify policy updates when generating the config file to reflect create/update policies --- config-generators/postgresql-commands.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config-generators/postgresql-commands.txt b/config-generators/postgresql-commands.txt index 07eb8aaa74..f249103253 100644 --- a/config-generators/postgresql-commands.txt +++ b/config-generators/postgresql-commands.txt @@ -58,8 +58,9 @@ update Publisher --config "dab-config.PostgreSql.json" --permissions "database_p update Publisher --config "dab-config.PostgreSql.json" --permissions "database_policy_tester:create" update Publisher --config "dab-config.PostgreSql.json" --permissions "database_policy_tester:update" --policy-database "@item.id ne 1234" update Stock --config "dab-config.PostgreSql.json" --permissions "authenticated:create,read,update,delete" --rest commodities --graphql true --relationship stocks_price --target.entity stocks_price --cardinality one -update Stock --config "dab-config.PostgreSql.json" --permissions "database_policy_tester:create,read" update Stock --config "dab-config.PostgreSql.json" --permissions "database_policy_tester:update" --policy-database "@item.pieceid ne 1" +update Stock --config "dab-config.PostgreSql.json" --permissions "database_policy_tester:create" --policy-database "@item.pieceid ne 6 and @item.piecesAvailable gt 0" +update Stock --config "dab-config.PostgreSql.json" --permissions "database_policy_tester:read" update Stock --config "dab-config.PostgreSql.json" --permissions "test_role_with_noread:create,update,delete" update Stock --config "dab-config.PostgreSql.json" --permissions "test_role_with_excluded_fields:create,update,delete" update Stock --config "dab-config.PostgreSql.json" --permissions "test_role_with_excluded_fields:read" --fields.exclude "categoryName" From 2333d8ed6fad4d802ff4b3ec57d52177594fb4d2 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 5 Jul 2026 12:00:15 -0700 Subject: [PATCH 07/11] Remove unnecessary using statements --- .../SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs | 1 - .../SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs index e13c0211b3..61db4e9b10 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Patch/PostgreSqlPatchApiTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs b/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs index 3eed9e3be5..c45b46066c 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Put/PostgreSqlPutApiTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; From e744cae98ff528d6783db95b91d0435a3f43b61e Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 5 Jul 2026 12:07:28 -0700 Subject: [PATCH 08/11] Update PostgreSQL verified snapshot policies --- ...ReadingRuntimeConfigForPostgreSql.verified.txt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index f7d781fe64..05f042b619 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -331,16 +331,19 @@ Role: database_policy_tester, Actions: [ { - Action: Update, - Policy: { - Database: @item.pieceid ne 1 - } + Action: Read }, { - Action: Create + Action: Create, + Policy: { + Database: @item.pieceid ne 6 and @item.piecesAvailable gt 0 + } }, { - Action: Read + Action: Update, + Policy: { + Database: @item.pieceid ne 1 + } } ] }, From 849b32f5397a7901cd7ca2157a1d3445619df37b Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 5 Jul 2026 12:38:51 -0700 Subject: [PATCH 09/11] Only pass in isFallbackToUpdate parameter for PostgreSQL --- src/Core/Resolvers/SqlMutationEngine.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/Resolvers/SqlMutationEngine.cs b/src/Core/Resolvers/SqlMutationEngine.cs index dfa032344f..10c005e66d 100644 --- a/src/Core/Resolvers/SqlMutationEngine.cs +++ b/src/Core/Resolvers/SqlMutationEngine.cs @@ -2056,7 +2056,9 @@ private async Task queryExecutor.GetMultipleResultSetsIfAnyAsync, dataSourceName, GetHttpContext(), - new List { prettyPrintPk, entityName, isFallbackToUpdate.ToString() }); + sqlMetadataProvider.GetDatabaseType() is DatabaseType.PostgreSQL + ? new List { prettyPrintPk, entityName, isFallbackToUpdate.ToString() } + : new List { prettyPrintPk, entityName }); } private Dictionary PrepareParameters(RestRequestContext context) @@ -2267,7 +2269,7 @@ private void AuthorizeEntityAndFieldsForMutation( /// } /// } /// - /// + /// /// Example 2 - Multiple items creation: /// /// mutation { From fa64d552924a0fb3b9ac56a73f5e2eac41aa90f7 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 5 Jul 2026 13:04:19 -0700 Subject: [PATCH 10/11] Update PostgreSQL insert query builder to account for create policy --- src/Core/Resolvers/PostgresQueryBuilder.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Core/Resolvers/PostgresQueryBuilder.cs b/src/Core/Resolvers/PostgresQueryBuilder.cs index da738f37d0..aa2119a417 100644 --- a/src/Core/Resolvers/PostgresQueryBuilder.cs +++ b/src/Core/Resolvers/PostgresQueryBuilder.cs @@ -68,11 +68,17 @@ public string Build(SqlQueryStructure structure) /// public string Build(SqlInsertStructure structure) { - string insertQuery = $"INSERT INTO {QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)} "; + string tableName = $"{QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)}"; + string dbPolicyPredicates = JoinPredicateStrings(structure.GetDbPolicyForOperation(EntityActionOperation.Create)); + string insertQuery = $"INSERT INTO {tableName} "; + if (structure.InsertColumns.Any()) { - insertQuery += $"({Build(structure.InsertColumns)}) " + - $"VALUES ({string.Join(", ", (structure.Values))}) "; + string insertColumns = Build(structure.InsertColumns); + string insertValues = dbPolicyPredicates.Equals(BASE_PREDICATE) + ? $"({insertColumns}) VALUES ({string.Join(", ", structure.Values)})" + : $"({insertColumns}) SELECT {insertColumns} FROM (SELECT {string.Join(", ", structure.InsertColumns.Zip(structure.Values, (col, val) => $"{val} AS {QuoteIdentifier(col)}"))}) AS T WHERE {dbPolicyPredicates}"; + insertQuery += insertValues; } else { From ddde125c74d6f42554fcf711f34517b9326df433 Mon Sep 17 00:00:00 2001 From: Arjun Narendra Date: Sun, 5 Jul 2026 15:26:39 -0700 Subject: [PATCH 11/11] Update unit test to reflect support for create policy for PostgreSQL --- src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs index 05561e4cf9..99252a7ba6 100644 --- a/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigValidationUnitTests.cs @@ -179,12 +179,12 @@ public void InvalidActionSpecifiedForARole(string dbPolicy, EntityActionOperatio /// /// Test that permission configuration validation fails when a database policy - /// is defined for the Create operation for mysql/postgresql and passes for mssql. + /// is defined for the Create operation for mysql and passes for mssql/postgresql. /// /// Database policy. /// Whether an error is expected. [DataTestMethod] - [DataRow(DatabaseType.PostgreSQL, "1 eq @item.col1", true, DisplayName = "Database Policy defined for Create fails for PostgreSQL")] + [DataRow(DatabaseType.PostgreSQL, "1 eq @item.col1", false, DisplayName = "Database Policy defined for Create passes for PostgreSQL")] [DataRow(DatabaseType.PostgreSQL, null, false, DisplayName = "Database Policy set as null for Create passes on PostgreSQL.")] [DataRow(DatabaseType.PostgreSQL, "", false, DisplayName = "Database Policy left empty for Create passes for PostgreSQL.")] [DataRow(DatabaseType.PostgreSQL, " ", false, DisplayName = "Database Policy only whitespace for Create passes for PostgreSQL.")] @@ -2792,7 +2792,7 @@ public void ValidateRuntimeBaseRouteSettings( /// This test checks that the final config used by runtime engine doesn't lose the directory information /// if provided by the user. /// It also validates that if config file is provided by the user, it will be used directly irrespective of - /// environment variable being set or not. + /// environment variable being set or not. /// When user doesn't provide a config file, we check if environment variable is set and if it is, we use /// the config file specified by the environment variable, else we use the default config file. ///