diff --git a/Frends.PostgreSQL.ExecuteQuery/CHANGELOG.md b/Frends.PostgreSQL.ExecuteQuery/CHANGELOG.md
index 552dff1..589618e 100644
--- a/Frends.PostgreSQL.ExecuteQuery/CHANGELOG.md
+++ b/Frends.PostgreSQL.ExecuteQuery/CHANGELOG.md
@@ -1,5 +1,19 @@
# Changelog
+## [2.0.0] - 2025-12-02
+### Added
+- Added `ExecuteType` parameter to Input class with options: Auto (default), ExecuteReader, and NonQuery.
+- Added support for INSERT/UPDATE/DELETE statements with RETURNING clause to return actual column values instead of just AffectedRows.
+- Added `ExecuteTypes` enum to provide explicit control over query execution behavior.
+
+### Changed
+- Modified query execution logic to use ExecuteType parameter instead of simple string parsing.
+- Auto mode now checks reader.FieldCount to determine if data is returned, providing more reliable detection than keyword matching.
+- Transaction handling now only applies to write operations (Auto and NonQuery modes), not read-only queries (ExecuteReader mode).
+
+### Fixed
+- Fixed issue where INSERT/UPDATE/DELETE with RETURNING clause only returned AffectedRows instead of the actual returned column values.
+
## [1.1.0] - 2024-08-23
### Changed
- Updated the Newtonsoft.Json package to version 13.0.3 and the Npgsql package to version 8.0.3.
diff --git a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.Tests/ExecuteQueryTests.cs b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.Tests/ExecuteQueryTests.cs
index 89139c0..8410109 100644
--- a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.Tests/ExecuteQueryTests.cs
+++ b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.Tests/ExecuteQueryTests.cs
@@ -36,7 +36,7 @@ public void TestSetup()
{
cmd.ExecuteNonQuery();
}
- using (var cmd = new NpgsqlCommand(@"INSERT INTO ""lista"" (Id, Selite) VALUES (1, 'Ensimmäinen'), (2, 'foobar'), (3, ''), (4, null)", conn))
+ using (var cmd = new NpgsqlCommand(@"INSERT INTO ""lista"" (Id, Selite) VALUES (1, 'Ensimm�inen'), (2, 'foobar'), (3, ''), (4, null)", conn))
{
cmd.ExecuteNonQuery();
}
@@ -113,4 +113,121 @@ public async Task TestInsertQuery()
result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
Assert.AreEqual("Viides", (string)result.QueryResult[0]["selite"]);
}
+
+ ///
+ /// Test INSERT with RETURNING clause.
+ ///
+ [Test]
+ public async Task TestInsertWithReturning()
+ {
+ var input = new Input
+ {
+ Query = @"INSERT INTO ""lista"" (Id, Selite) VALUES (6, 'Kuudes') RETURNING Id, Selite",
+ Parameters = null,
+ ConnectionString = _connection
+ };
+
+ var result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+
+ // Should return the inserted values, not AffectedRows
+ Assert.IsNotNull(result.QueryResult);
+ Assert.AreEqual(1, result.QueryResult.Count);
+ Assert.AreEqual(6, (int)result.QueryResult[0]["id"]);
+ Assert.AreEqual("Kuudes", (string)result.QueryResult[0]["selite"]);
+ }
+
+ ///
+ /// Test UPDATE with RETURNING clause.
+ ///
+ [Test]
+ public async Task TestUpdateWithReturning()
+ {
+ var input = new Input
+ {
+ Query = @"UPDATE ""lista"" SET Selite = 'Updated' WHERE Id = 1 RETURNING Id, Selite",
+ Parameters = null,
+ ConnectionString = _connection
+ };
+
+ var result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+
+ // Should return the updated values
+ Assert.IsNotNull(result.QueryResult);
+ Assert.AreEqual(1, result.QueryResult.Count);
+ Assert.AreEqual(1, (int)result.QueryResult[0]["id"]);
+ Assert.AreEqual("Updated", (string)result.QueryResult[0]["selite"]);
+
+ // Restore original value
+ input.Query = @"UPDATE ""lista"" SET Selite = 'Ensimm�inen' WHERE Id = 1";
+ await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+ }
+
+ ///
+ /// Test DELETE with RETURNING clause.
+ ///
+ [Test]
+ public async Task TestDeleteWithReturning()
+ {
+ // First, insert a row to delete
+ var input = new Input
+ {
+ Query = @"INSERT INTO ""lista"" (Id, Selite) VALUES (7, 'Seitsem�s')",
+ Parameters = null,
+ ConnectionString = _connection
+ };
+ await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+
+ // Now delete it with RETURNING
+ input.Query = @"DELETE FROM ""lista"" WHERE Id = 7 RETURNING Id, Selite";
+ var result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+
+ // Should return the deleted values
+ Assert.IsNotNull(result.QueryResult);
+ Assert.AreEqual(1, result.QueryResult.Count);
+ Assert.AreEqual(7, (int)result.QueryResult[0]["id"]);
+ Assert.AreEqual("Seitsem�s", (string)result.QueryResult[0]["selite"]);
+ }
+
+ ///
+ /// Test INSERT with RETURNING clause using ExecuteType.ExecuteReader explicitly.
+ ///
+ [Test]
+ public async Task TestInsertWithReturningExplicit()
+ {
+ var input = new Input
+ {
+ Query = @"INSERT INTO ""lista"" (Id, Selite) VALUES (8, 'Kahdeksas') RETURNING Id, Selite",
+ Parameters = null,
+ ConnectionString = _connection,
+ ExecuteType = ExecuteTypes.ExecuteReader
+ };
+
+ var result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+
+ // Should return the inserted values
+ Assert.IsNotNull(result.QueryResult);
+ Assert.AreEqual(1, result.QueryResult.Count);
+ Assert.AreEqual(8, (int)result.QueryResult[0]["id"]);
+ Assert.AreEqual("Kahdeksas", (string)result.QueryResult[0]["selite"]);
+ }
+
+ ///
+ /// Test INSERT without RETURNING using ExecuteType.NonQuery explicitly.
+ ///
+ [Test]
+ public async Task TestInsertWithNonQueryExplicit()
+ {
+ var input = new Input
+ {
+ Query = @"INSERT INTO ""lista"" (Id, Selite) VALUES (9, 'Yhdeks�s')",
+ Parameters = null,
+ ConnectionString = _connection,
+ ExecuteType = ExecuteTypes.NonQuery
+ };
+
+ var result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
+
+ // Should return affected rows
+ Assert.AreEqual(1, (int)result.QueryResult["AffectedRows"]);
+ }
}
diff --git a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/ExecuteTypes.cs b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/ExecuteTypes.cs
new file mode 100644
index 0000000..5261db7
--- /dev/null
+++ b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/ExecuteTypes.cs
@@ -0,0 +1,28 @@
+namespace Frends.PostgreSQL.ExecuteQuery.Definitions;
+
+///
+/// Specifies how a command string is interpreted.
+///
+public enum ExecuteTypes
+{
+ ///
+ /// Auto-detect based on query structure.
+ /// Uses ExecuteReader for queries that return data (SELECT or queries with RETURNING clause).
+ /// Uses NonQuery for INSERT, UPDATE, DELETE statements without RETURNING clause.
+ ///
+ Auto,
+
+ ///
+ /// Execute the query without expecting any result set.
+ /// Use for INSERT, UPDATE, DELETE statements without RETURNING clause.
+ /// Returns the number of rows affected.
+ ///
+ NonQuery,
+
+ ///
+ /// Execute the query and return the result set.
+ /// Use for SELECT queries or INSERT/UPDATE/DELETE with RETURNING clause.
+ /// Returns the data rows.
+ ///
+ ExecuteReader
+}
diff --git a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/Input.cs b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/Input.cs
index e6ca37d..85b165e 100644
--- a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/Input.cs
+++ b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Definitions/Input.cs
@@ -32,4 +32,14 @@ public class Input
[PasswordPropertyText]
public string ConnectionString { get; set; }
+ ///
+ /// Specifies how a command string is interpreted.
+ /// Auto: Automatically detects if the query returns data (SELECT or RETURNING clause) and uses ExecuteReader, otherwise uses NonQuery.
+ /// ExecuteReader: Use this to execute queries that return a result set (SELECT or INSERT/UPDATE/DELETE with RETURNING clause).
+ /// NonQuery: Use this to execute commands that don't return a result set (INSERT, UPDATE, DELETE without RETURNING). Returns the number of affected rows.
+ ///
+ /// ExecuteTypes.Auto
+ [DefaultValue(ExecuteTypes.Auto)]
+ public ExecuteTypes ExecuteType { get; set; }
+
}
diff --git a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/ExecuteQuery.cs b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/ExecuteQuery.cs
index d7b537d..8b81501 100644
--- a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/ExecuteQuery.cs
+++ b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/ExecuteQuery.cs
@@ -55,21 +55,55 @@ public static async Task ExecuteQuery([PropertyTab] Input input, [Proper
}
}
- // Execute command.
-
- if (input.Query.ToLower().Contains("select"))
- {
- var reader = await cmd.ExecuteReaderAsync(cancellationToken);
- result = new Result(reader.ToJson(cancellationToken));
- }
- else
+ // Execute command based on ExecuteType.
+ switch (input.ExecuteType)
{
- var transaction = conn.BeginTransaction(GetIsolationLevel(options.SqlTransactionIsolationLevel));
- cmd.Transaction = transaction;
- var rows = await cmd.ExecuteNonQueryAsync(cancellationToken);
- await transaction.CommitAsync(cancellationToken);
- transaction.Dispose();
- result = new Result(JToken.FromObject(new { AffectedRows = rows }));
+ case ExecuteTypes.Auto:
+ // Auto-detect: Try ExecuteReader first to check if data is returned
+ {
+ using var transaction = conn.BeginTransaction(GetIsolationLevel(options.SqlTransactionIsolationLevel));
+ cmd.Transaction = transaction;
+
+ using (var reader = await cmd.ExecuteReaderAsync(cancellationToken))
+ {
+ // Check if the query returned any data (has columns)
+ if (reader.FieldCount > 0)
+ {
+ // Query returned data (SELECT or RETURNING clause)
+ result = new Result(reader.ToJson(cancellationToken));
+ }
+ else
+ {
+ // Query did not return data, use RecordsAffected
+ result = new Result(JToken.FromObject(new { AffectedRows = reader.RecordsAffected }));
+ }
+ } // Reader is disposed here
+
+ await transaction.CommitAsync(cancellationToken);
+ }
+ break;
+
+ case ExecuteTypes.ExecuteReader:
+ // Explicitly return data - no transaction needed for read-only queries
+ using (var reader = await cmd.ExecuteReaderAsync(cancellationToken))
+ {
+ result = new Result(reader.ToJson(cancellationToken));
+ }
+ break;
+
+ case ExecuteTypes.NonQuery:
+ // Execute without returning data - use transaction
+ {
+ using var transaction = conn.BeginTransaction(GetIsolationLevel(options.SqlTransactionIsolationLevel));
+ cmd.Transaction = transaction;
+ var rows = await cmd.ExecuteNonQueryAsync(cancellationToken);
+ result = new Result(JToken.FromObject(new { AffectedRows = rows }));
+ await transaction.CommitAsync(cancellationToken);
+ }
+ break;
+
+ default:
+ throw new ArgumentException($"Unsupported ExecuteType: {input.ExecuteType}");
}
await conn.CloseAsync();
diff --git a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.csproj b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.csproj
index 2af857f..2ab6208 100644
--- a/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.csproj
+++ b/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery/Frends.PostgreSQL.ExecuteQuery.csproj
@@ -7,7 +7,7 @@
true
Frends.PostgreSQL.ExecuteQuery
Frends.PostgreSQL.ExecuteQuery
- 1.1.0
+ 2.0.0
Frends
Frends
Frends