Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Frends.PostgreSQL.ExecuteQuery/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -113,4 +113,121 @@ public async Task TestInsertQuery()
result = await PostgreSQL.ExecuteQuery(input, _options, new CancellationToken());
Assert.AreEqual("Viides", (string)result.QueryResult[0]["selite"]);
}

/// <summary>
/// Test INSERT with RETURNING clause.
/// </summary>
[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"]);
}

/// <summary>
/// Test UPDATE with RETURNING clause.
/// </summary>
[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());
}

/// <summary>
/// Test DELETE with RETURNING clause.
/// </summary>
[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"]);
}

/// <summary>
/// Test INSERT with RETURNING clause using ExecuteType.ExecuteReader explicitly.
/// </summary>
[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"]);
}

/// <summary>
/// Test INSERT without RETURNING using ExecuteType.NonQuery explicitly.
/// </summary>
[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"]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Frends.PostgreSQL.ExecuteQuery.Definitions;

/// <summary>
/// Specifies how a command string is interpreted.
/// </summary>
public enum ExecuteTypes
{
/// <summary>
/// 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.
/// </summary>
Auto,

/// <summary>
/// Execute the query without expecting any result set.
/// Use for INSERT, UPDATE, DELETE statements without RETURNING clause.
/// Returns the number of rows affected.
/// </summary>
NonQuery,

/// <summary>
/// Execute the query and return the result set.
/// Use for SELECT queries or INSERT/UPDATE/DELETE with RETURNING clause.
/// Returns the data rows.
/// </summary>
ExecuteReader
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,14 @@ public class Input
[PasswordPropertyText]
public string ConnectionString { get; set; }

/// <summary>
/// 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.
/// </summary>
/// <example>ExecuteTypes.Auto</example>
[DefaultValue(ExecuteTypes.Auto)]
public ExecuteTypes ExecuteType { get; set; }

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,55 @@ public static async Task<Result> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<IncludeSource>true</IncludeSource>
<AssemblyName>Frends.PostgreSQL.ExecuteQuery</AssemblyName>
<RootNamespace>Frends.PostgreSQL.ExecuteQuery</RootNamespace>
<Version>1.1.0</Version>
<Version>2.0.0</Version>
<Authors>Frends</Authors>
<Copyright>Frends</Copyright>
<Company>Frends</Company>
Expand Down
Loading