@@ -17,6 +17,8 @@ public class PostgresQueryBuilder : BaseSqlQueryBuilder, IQueryBuilder
1717 private const string UPSERT_IDENTIFIER_COLUMN_NAME = "___upsert_op___" ;
1818 private const string INSERT_UPSERT = "inserted" ;
1919 private const string UPDATE_UPSERT = "updated" ;
20+ public const string COUNT_ROWS_WITH_GIVEN_PK = "cnt_rows_to_update" ;
21+ public const string IS_FALLBACK_TO_UPDATE = "is_fallback_to_update" ;
2022
2123 private static DbCommandBuilder _builder = new NpgsqlCommandBuilder ( ) ;
2224
@@ -67,11 +69,17 @@ public string Build(SqlQueryStructure structure)
6769 /// <inheritdoc />
6870 public string Build ( SqlInsertStructure structure )
6971 {
70- string insertQuery = $ "INSERT INTO { QuoteIdentifier ( structure . DatabaseObject . SchemaName ) } .{ QuoteIdentifier ( structure . DatabaseObject . Name ) } ";
72+ string tableName = $ "{ QuoteIdentifier ( structure . DatabaseObject . SchemaName ) } .{ QuoteIdentifier ( structure . DatabaseObject . Name ) } ";
73+ string dbPolicyPredicates = JoinPredicateStrings ( structure . GetDbPolicyForOperation ( EntityActionOperation . Create ) ) ;
74+ string insertQuery = $ "INSERT INTO { tableName } ";
75+
7176 if ( structure . InsertColumns . Any ( ) )
7277 {
73- insertQuery += $ "({ Build ( structure . InsertColumns ) } ) " +
74- $ "VALUES ({ string . Join ( ", " , ( structure . Values ) ) } ) ";
78+ string insertColumns = Build ( structure . InsertColumns ) ;
79+ string insertValues = dbPolicyPredicates . Equals ( BASE_PREDICATE )
80+ ? $ "({ insertColumns } ) VALUES ({ string . Join ( ", " , structure . Values ) } )"
81+ : $ "({ insertColumns } ) SELECT { insertColumns } FROM (SELECT { string . Join ( ", " , structure . InsertColumns . Zip ( structure . Values , ( col , val ) => $ "{ val } AS { QuoteIdentifier ( col ) } ") ) } ) AS T WHERE { dbPolicyPredicates } ";
82+ insertQuery += insertValues ;
7583 }
7684 else
7785 {
@@ -117,25 +125,53 @@ public string Build(SqlUpsertQueryStructure structure)
117125 {
118126 // https://stackoverflow.com/questions/42668720/check-if-postgres-query-inserted-or-updated-via-upsert
119127 // relying on xmax to detect insert vs update breaks for views
120- string updatePredicates = JoinPredicateStrings ( Build ( structure . Predicates ) , structure . GetDbPolicyForOperation ( EntityActionOperation . Update ) ) ;
121- string updateQuery = $ "UPDATE { QuoteIdentifier ( structure . DatabaseObject . SchemaName ) } .{ QuoteIdentifier ( structure . DatabaseObject . Name ) } " +
128+ string tableName = $ "{ QuoteIdentifier ( structure . DatabaseObject . SchemaName ) } .{ QuoteIdentifier ( structure . DatabaseObject . Name ) } ";
129+ string pkPredicates = Build ( structure . Predicates ) ;
130+ string isFallbackToUpdateSqlLiteral = structure . IsFallbackToUpdate ? "TRUE" : "FALSE" ;
131+
132+ // RS1: COUNT of rows matching PK (no policy) — used to distinguish
133+ // "row doesn't exist" from "row exists but policy blocked" in the executor.
134+ string countQuery = $ "SELECT COUNT(*) AS { COUNT_ROWS_WITH_GIVEN_PK } , " +
135+ $ "{ isFallbackToUpdateSqlLiteral } AS { IS_FALLBACK_TO_UPDATE } " +
136+ $ "FROM { tableName } WHERE { pkPredicates } ";
137+
138+ string updatePredicates = JoinPredicateStrings ( pkPredicates , structure . GetDbPolicyForOperation ( EntityActionOperation . Update ) ) ;
139+ string updateQuery = $ "UPDATE { tableName } " +
122140 $ "SET { Build ( structure . UpdateOperations , ", " ) } " +
123141 $ "WHERE { updatePredicates } " +
124142 $ "RETURNING { Build ( structure . OutputColumns ) } , '{ UPDATE_UPSERT } ' AS { UPSERT_IDENTIFIER_COLUMN_NAME } ";
125143
126144 if ( structure . IsFallbackToUpdate )
127145 {
128- return updateQuery + ";" ;
146+ // RS2: UPDATE only — no INSERT branch for autogen PK or missing required columns.
147+ return $ "{ countQuery } ; { updateQuery } ;";
129148 }
130149 else
131150 {
132- return $ "WITH update_cte AS ( { updateQuery } ), insert_cte AS ( " +
133- $ "INSERT INTO { QuoteIdentifier ( structure . DatabaseObject . SchemaName ) } .{ QuoteIdentifier ( structure . DatabaseObject . Name ) } ({ Build ( structure . InsertColumns ) } ) " +
134- $ "SELECT { string . Join ( ", " , ( structure . Values ) ) } " +
135- $ "WHERE NOT EXISTS (SELECT 1 FROM update_cte) " +
151+ // INSERT only runs when row doesn't exist (pkPredicates match nothing)
152+ // AND the create policy (if any) is satisfied.
153+ string insertPredicates = JoinPredicateStrings (
154+ $ "NOT EXISTS (SELECT 1 FROM { tableName } WHERE { pkPredicates } )",
155+ structure . GetDbPolicyForOperation ( EntityActionOperation . Create ) ) ;
156+
157+ // Alias each value with its column name so that policy predicates referencing
158+ // column names (e.g. "pieceid" != @param) can be resolved in the WHERE clause.
159+ // Using SELECT ... FROM (SELECT @p1 AS col1, ...) AS T avoids both the VALUES(NULL)
160+ // type inference issue and the unnamed-column resolution issue.
161+ string namedValues = string . Join ( ", " ,
162+ structure . InsertColumns . Zip ( structure . Values ,
163+ ( col , val ) => $ "{ val } AS { QuoteIdentifier ( col ) } ") ) ;
164+
165+ // RS2: CTE that attempts UPDATE first; falls through to INSERT only when row is absent.
166+ string cteQuery = $ "WITH update_cte AS ( { updateQuery } ), insert_cte AS ( " +
167+ $ "INSERT INTO { tableName } ({ Build ( structure . InsertColumns ) } ) " +
168+ $ "SELECT { Build ( structure . InsertColumns ) } FROM (SELECT { namedValues } ) AS T " +
169+ $ "WHERE { insertPredicates } " +
136170 $ "RETURNING { Build ( structure . OutputColumns ) } , '{ INSERT_UPSERT } ' AS { UPSERT_IDENTIFIER_COLUMN_NAME } ) " +
137- $ "SELECT { BuildListOfLabels ( structure . OutputColumns ) } , { UPSERT_IDENTIFIER_COLUMN_NAME } FROM update_cte UNION " +
171+ $ "SELECT { BuildListOfLabels ( structure . OutputColumns ) } , { UPSERT_IDENTIFIER_COLUMN_NAME } FROM update_cte UNION ALL " +
138172 $ "SELECT { BuildListOfLabels ( structure . OutputColumns ) } , { UPSERT_IDENTIFIER_COLUMN_NAME } FROM insert_cte;";
173+
174+ return $ "{ countQuery } ; { cteQuery } ";
139175 }
140176 }
141177
0 commit comments