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