• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / FluentCommand / 26559531993

28 May 2026 06:51AM UTC coverage: 54.902% (+0.5%) from 54.377%
26559531993

push

github

pwelter34
Add UPSERT support to query builder and SQL generators

1405 of 3347 branches covered (41.98%)

Branch coverage included in aggregate %.

179 of 227 new or added lines in 7 files covered. (78.85%)

4341 of 7119 relevant lines covered (60.98%)

308.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.53
/src/FluentCommand/Query/Generators/PostgresqlGenerator.cs
1
using FluentCommand.Extensions;
2
using FluentCommand.Internal;
3

4
namespace FluentCommand.Query.Generators;
5

6
/// <summary>
7
/// Provides a SQL generator for PostgreSQL, implementing SQL statement and expression generation
8
/// with PostgreSQL-specific syntax and conventions.
9
/// </summary>
10
public class PostgreSqlGenerator : SqlServerGenerator
11
{
12
    /// <summary>
13
    /// Builds a SQL INSERT statement for PostgreSQL, including support for RETURNING and comments.
14
    /// </summary>
15
    /// <param name="insertStatement">The <see cref="InsertStatement"/> containing the INSERT statement configuration.</param>
16
    /// <returns>A SQL INSERT statement string for PostgreSQL.</returns>
17
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="insertStatement"/> is <c>null</c>.</exception>
18
    /// <exception cref="ArgumentException">Thrown if the table or values are not specified.</exception>
19
    public override string BuildInsert(InsertStatement insertStatement)
20
    {
21
        if (insertStatement is null)
3!
22
            throw new ArgumentNullException(nameof(insertStatement));
×
23

24
        if (insertStatement.TableExpression == null)
3!
25
            throw new ArgumentException("No table specified to insert into", nameof(insertStatement));
×
26

27
        if (insertStatement.ValueExpressions == null || insertStatement.ValueExpressions.Count == 0)
3!
28
            throw new ArgumentException("No values specified for insert", nameof(insertStatement));
×
29

30
        var insertBuilder = StringBuilderCache.Acquire();
3✔
31

32
        if (insertStatement.CommentExpressions?.Count > 0)
3!
33
        {
34
            insertBuilder
3✔
35
                .AppendJoin(Environment.NewLine, insertStatement.CommentExpressions)
3✔
36
                .AppendLine();
3✔
37
        }
38

39
        var table = TableExpression(insertStatement.TableExpression);
3✔
40
        insertBuilder
3✔
41
            .Append("INSERT INTO ")
3✔
42
            .Append(table);
3✔
43

44
        if (insertStatement.ColumnExpressions?.Count > 0)
3!
45
        {
46
            insertBuilder
3✔
47
                .Append(" (")
3✔
48
                .AppendJoin(", ", insertStatement.ColumnExpressions.Select(ColumnExpression))
3✔
49
                .Append(")");
3✔
50
        }
51

52
        insertBuilder
3✔
53
            .AppendLine()
3✔
54
            .Append("VALUES ")
3✔
55
            .Append("(")
3✔
56
            .AppendJoin(", ", insertStatement.ValueExpressions)
3✔
57
            .Append(")");
3✔
58

59
        if (insertStatement.OutputExpressions?.Count > 0)
3!
60
        {
61
            insertBuilder
3✔
62
                .AppendLine()
3✔
63
                .Append("RETURNING ")
3✔
64
                .AppendJoin(", ", insertStatement.OutputExpressions.Select(ColumnExpression));
3✔
65
        }
66

67
        insertBuilder.AppendLine(";");
3✔
68

69
        return StringBuilderCache.ToString(insertBuilder);
3✔
70
    }
71

72
    /// <summary>
73
    /// Builds a SQL UPSERT statement for PostgreSQL using ON CONFLICT syntax, including support for RETURNING and comments.
74
    /// </summary>
75
    /// <param name="upsertStatement">The <see cref="UpsertStatement"/> containing the UPSERT statement configuration.</param>
76
    /// <returns>A SQL UPSERT statement string for PostgreSQL.</returns>
77
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="upsertStatement"/> is <c>null</c>.</exception>
78
    /// <exception cref="ArgumentException">Thrown if the table, values, keys, or update values are not specified.</exception>
79
    public override string BuildUpsert(UpsertStatement upsertStatement)
80
    {
81
        ValidateUpsert(upsertStatement);
7✔
82

83
        var upsertBuilder = StringBuilderCache.Acquire();
7✔
84

85
        if (upsertStatement.CommentExpressions?.Count > 0)
7!
86
        {
NEW
87
            upsertBuilder
×
NEW
88
                .AppendJoin(Environment.NewLine, upsertStatement.CommentExpressions)
×
NEW
89
                .AppendLine();
×
90
        }
91

92
        var table = TableExpression(upsertStatement.TableExpression);
7✔
93
        upsertBuilder
7✔
94
            .Append("INSERT INTO ")
7✔
95
            .Append(table)
7✔
96
            .Append(" (")
7✔
97
            .AppendJoin(", ", upsertStatement.ColumnExpressions.Select(ColumnExpression))
7✔
98
            .AppendLine(")")
7✔
99
            .Append("VALUES (")
7✔
100
            .AppendJoin(", ", upsertStatement.ValueExpressions)
7✔
101
            .AppendLine(")")
7✔
102
            .Append("ON CONFLICT (")
7✔
103
            .AppendJoin(", ", upsertStatement.KeyExpressions.Select(ColumnExpression))
7✔
104
            .AppendLine(") DO UPDATE")
7✔
105
            .Append("SET ")
7✔
106
            .AppendJoin(", ", upsertStatement.UpdateExpressions.Select(u => $"{ColumnExpression(u)} = EXCLUDED.{ColumnExpression(u)}"));
7✔
107

108
        if (upsertStatement.OutputExpressions?.Count > 0)
7!
109
        {
110
            upsertBuilder
3✔
111
                .AppendLine()
3✔
112
                .Append("RETURNING ")
3✔
113
                .AppendJoin(", ", upsertStatement.OutputExpressions.Select(ColumnExpression));
3✔
114
        }
115

116
        upsertBuilder.AppendLine(";");
7✔
117

118
        return StringBuilderCache.ToString(upsertBuilder);
7✔
119
    }
120

121
    /// <summary>
122
    /// Builds a SQL UPDATE statement for PostgreSQL, including support for FROM, JOIN, WHERE, RETURNING, and comments.
123
    /// </summary>
124
    /// <param name="updateStatement">The <see cref="UpdateStatement"/> containing the UPDATE statement configuration.</param>
125
    /// <returns>A SQL UPDATE statement string for PostgreSQL.</returns>
126
    /// <exception cref="ArgumentException">Thrown if the table or update values are not specified.</exception>
127
    public override string BuildUpdate(UpdateStatement updateStatement)
128
    {
129
        if (updateStatement.UpdateExpressions == null || updateStatement.UpdateExpressions.Count == 0)
1!
130
            throw new ArgumentException("No values specified for update", nameof(updateStatement));
×
131

132
        var updateBuilder = StringBuilderCache.Acquire();
1✔
133

134
        if (updateStatement.CommentExpressions?.Count > 0)
1!
135
        {
136
            updateBuilder
1✔
137
                .AppendJoin(Environment.NewLine, updateStatement.CommentExpressions)
1✔
138
                .AppendLine();
1✔
139
        }
140

141
        var table = TableExpression(updateStatement.TableExpression);
1✔
142

143
        updateBuilder
1✔
144
            .Append("UPDATE ")
1✔
145
            .Append(table)
1✔
146
            .AppendLine()
1✔
147
            .Append("SET ")
1✔
148
            .AppendJoin(", ", updateStatement.UpdateExpressions.Select(UpdateExpression));
1✔
149

150
        if (updateStatement.FromExpressions?.Count > 0)
1!
151
        {
152
            updateBuilder
×
153
                .AppendLine()
×
154
                .Append("FROM ")
×
155
                .AppendJoin(", ", updateStatement.FromExpressions.Select(TableExpression));
×
156
        }
157

158
        if (updateStatement.JoinExpressions?.Count > 0)
1!
159
        {
160
            foreach (var joinExpression in updateStatement.JoinExpressions)
×
161
            {
162
                updateBuilder
×
163
                    .AppendLine()
×
164
                    .Append(JoinExpression(joinExpression));
×
165
            }
166
        }
167

168
        if (updateStatement.WhereExpressions?.Count > 0)
1!
169
        {
170
            updateBuilder
1✔
171
                .AppendLine()
1✔
172
                .Append("WHERE ")
1✔
173
                .Append("(")
1✔
174
                .AppendJoin(" AND ", updateStatement.WhereExpressions.Select(WhereExpression))
1✔
175
                .Append(")");
1✔
176
        }
177

178
        if (updateStatement.OutputExpressions?.Count > 0)
1!
179
        {
180
            updateBuilder
1✔
181
                .AppendLine()
1✔
182
                .Append("RETURNING ")
1✔
183
                .AppendJoin(", ", updateStatement.OutputExpressions.Select(ColumnExpression));
1✔
184
        }
185

186
        updateBuilder.AppendLine(";");
1✔
187

188
        return StringBuilderCache.ToString(updateBuilder);
1✔
189
    }
190

191
    /// <summary>
192
    /// Builds a SQL DELETE statement for PostgreSQL, including support for FROM, JOIN, WHERE, RETURNING, and comments.
193
    /// </summary>
194
    /// <param name="deleteStatement">The <see cref="DeleteStatement"/> containing the DELETE statement configuration.</param>
195
    /// <returns>A SQL DELETE statement string for PostgreSQL.</returns>
196
    /// <exception cref="ArgumentException">Thrown if the table is not specified.</exception>
197
    public override string BuildDelete(DeleteStatement deleteStatement)
198
    {
199
        if (deleteStatement.TableExpression == null)
1!
200
            throw new ArgumentException("No table specified to delete from", nameof(deleteStatement));
×
201

202
        var deleteBuilder = StringBuilderCache.Acquire();
1✔
203

204
        if (deleteStatement.CommentExpressions?.Count > 0)
1!
205
        {
206
            deleteBuilder
1✔
207
                .AppendJoin(Environment.NewLine, deleteStatement.CommentExpressions)
1✔
208
                .AppendLine();
1✔
209
        }
210

211
        var table = TableExpression(deleteStatement.TableExpression);
1✔
212

213
        deleteBuilder
1✔
214
            .Append("DELETE FROM ")
1✔
215
            .Append(table);
1✔
216

217
        if (deleteStatement.FromExpressions?.Count > 0)
1!
218
        {
219
            deleteBuilder
×
220
                .AppendLine()
×
221
                .Append("FROM ")
×
222
                .AppendJoin(", ", deleteStatement.FromExpressions.Select(TableExpression));
×
223
        }
224

225
        if (deleteStatement.JoinExpressions?.Count > 0)
1!
226
        {
227
            foreach (var joinExpression in deleteStatement.JoinExpressions)
×
228
            {
229
                deleteBuilder
×
230
                    .AppendLine()
×
231
                    .Append(JoinExpression(joinExpression));
×
232
            }
233
        }
234

235
        if (deleteStatement.WhereExpressions?.Count > 0)
1!
236
        {
237
            deleteBuilder
1✔
238
                .AppendLine()
1✔
239
                .Append("WHERE ")
1✔
240
                .Append("(")
1✔
241
                .AppendJoin(" AND ", deleteStatement.WhereExpressions.Select(WhereExpression))
1✔
242
                .Append(")");
1✔
243
        }
244

245
        if (deleteStatement.OutputExpressions?.Count > 0)
1!
246
        {
247
            deleteBuilder
1✔
248
                .AppendLine()
1✔
249
                .Append("RETURNING ")
1✔
250
                .AppendJoin(", ", deleteStatement.OutputExpressions.Select(ColumnExpression));
1✔
251
        }
252

253
        deleteBuilder.AppendLine(";");
1✔
254

255
        return StringBuilderCache.ToString(deleteBuilder);
1✔
256
    }
257

258
    /// <summary>
259
    /// Builds a SQL table expression for PostgreSQL, omitting schema (PostgreSQL does not support schema in the same way as SQL Server).
260
    /// </summary>
261
    /// <param name="tableExpression">The <see cref="TableExpression"/> representing the table.</param>
262
    /// <returns>A SQL table expression string for PostgreSQL.</returns>
263
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="tableExpression"/> is <c>null</c>.</exception>
264
    public override string TableExpression(TableExpression tableExpression)
265
    {
266
        if (tableExpression is null)
22!
267
            throw new ArgumentNullException(nameof(tableExpression));
×
268

269
        // sqlite doesn't support schema
270
        tableExpression = tableExpression with { TableSchema = null };
22✔
271

272
        return base.TableExpression(tableExpression);
22✔
273
    }
274

275
    /// <summary>
276
    /// Builds a SQL WHERE expression for PostgreSQL, handling various filter operators and parameterization.
277
    /// </summary>
278
    /// <param name="whereExpression">The <see cref="WhereExpression"/> representing the condition.</param>
279
    /// <returns>A SQL WHERE expression string for PostgreSQL.</returns>
280
    /// <exception cref="ArgumentNullException">Thrown if <paramref name="whereExpression"/> is <c>null</c>.</exception>
281
    /// <exception cref="ArgumentException">Thrown if required properties are not specified.</exception>
282
    public override string WhereExpression(WhereExpression whereExpression)
283
    {
284
        if (whereExpression is null)
11!
285
            throw new ArgumentNullException(nameof(whereExpression));
×
286

287
        if (string.IsNullOrWhiteSpace(whereExpression.ColumnName))
11!
288
            throw new ArgumentException($"'{nameof(whereExpression.ColumnName)}' property cannot be null or empty.", nameof(whereExpression));
×
289

290
        if (whereExpression.IsRaw)
11✔
291
            return whereExpression.ColumnName;
1✔
292

293
        var parameterlessFilters = new[] { FilterOperators.IsNull, FilterOperators.IsNotNull };
10✔
294
        if (!parameterlessFilters.Contains(whereExpression.FilterOperator) && string.IsNullOrWhiteSpace(whereExpression.ParameterName))
10!
295
            throw new ArgumentException($"'{nameof(whereExpression.ParameterName)}' property cannot be null or empty.", nameof(whereExpression));
×
296

297
        var columnName = ColumnExpression(whereExpression);
10✔
298

299
        return whereExpression.FilterOperator switch
10!
300
        {
10✔
301
            FilterOperators.StartsWith => $"{columnName} LIKE {whereExpression.ParameterName} || '%'",
×
302
            FilterOperators.EndsWith => $"{columnName} LIKE '%' || {whereExpression.ParameterName}",
×
303
            FilterOperators.Contains => $"{columnName} LIKE '%' || {whereExpression.ParameterName} || '%'",
2✔
304
            FilterOperators.Equal => $"{columnName} = {whereExpression.ParameterName}",
6✔
305
            FilterOperators.NotEqual => $"{columnName} != {whereExpression.ParameterName}",
1✔
306
            FilterOperators.LessThan => $"{columnName} < {whereExpression.ParameterName}",
×
307
            FilterOperators.LessThanOrEqual => $"{columnName} <= {whereExpression.ParameterName}",
×
308
            FilterOperators.GreaterThan => $"{columnName} > {whereExpression.ParameterName}",
×
309
            FilterOperators.GreaterThanOrEqual => $"{columnName} >= {whereExpression.ParameterName}",
×
310
            FilterOperators.IsNull => $"{columnName} IS NULL",
×
311
            FilterOperators.IsNotNull => $"{columnName} IS NOT NULL",
×
312
            FilterOperators.In => $"{columnName} IN ({whereExpression.ParameterName})",
1✔
313
            _ => $"{columnName} = {whereExpression.ParameterName}",
×
314
        };
10✔
315
    }
316

317
    /// <summary>
318
    /// Builds a SQL LIMIT/OFFSET expression for PostgreSQL.
319
    /// </summary>
320
    /// <param name="limitExpression">The <see cref="LimitExpression"/> representing the limit and offset.</param>
321
    /// <returns>A SQL LIMIT/OFFSET expression string for PostgreSQL, or an empty string if not applicable.</returns>
322
    public override string LimitExpression(LimitExpression limitExpression)
323
    {
324
        if (limitExpression is null || limitExpression.Size == 0)
3!
325
            return string.Empty;
×
326

327
        return $"LIMIT {limitExpression.Size} OFFSET {limitExpression.Offset}";
3✔
328
    }
329

330
    /// <summary>
331
    /// Quotes an identifier (such as a table or column name) for PostgreSQL, using double quotes.
332
    /// </summary>
333
    /// <param name="name">The identifier to quote.</param>
334
    /// <returns>The quoted identifier, or the original name if quoting is not required.</returns>
335
    public override string QuoteIdentifier(string name)
336
    {
337
        if (name.IsNullOrWhiteSpace())
255!
338
            return string.Empty;
×
339

340
        if (name == "*")
255✔
341
            return name;
1✔
342

343
        if (name.StartsWith("\"") && name.EndsWith("\""))
254!
344
            return name;
×
345

346
        return "\"" + name.Replace("\"", "\"\"") + "\"";
254✔
347
    }
348

349
    /// <summary>
350
    /// Parses a quoted identifier and returns the unquoted name for PostgreSQL.
351
    /// </summary>
352
    /// <param name="name">The quoted identifier.</param>
353
    /// <returns>The unquoted identifier name.</returns>
354
    public override string ParseIdentifier(string name)
355
    {
356
        if (name.StartsWith("\"") && name.EndsWith("\""))
×
357
            return name.Substring(1, name.Length - 2);
×
358

359
        return name;
×
360
    }
361
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc