diff --git a/src/EntityFramework.Firebird.Tests/EntityFrameworkTestsBase.cs b/src/EntityFramework.Firebird.Tests/EntityFrameworkTestsBase.cs index bdf893bf..d41e80aa 100644 --- a/src/EntityFramework.Firebird.Tests/EntityFrameworkTestsBase.cs +++ b/src/EntityFramework.Firebird.Tests/EntityFrameworkTestsBase.cs @@ -27,7 +27,7 @@ public abstract class EntityFrameworkTestsBase : FbTestsBase { static EntityFrameworkTestsBase() { -#if NET6_0 +#if NET6_0_OR_GREATER System.Data.Common.DbProviderFactories.RegisterFactory(FbProviderServices.ProviderInvariantName, FirebirdClientFactory.Instance); #endif DbConfiguration.SetConfiguration(new FbTestDbContext.Conf()); diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbBatchCommandTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbBatchCommandTests.cs new file mode 100644 index 00000000..ec003bd6 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbBatchCommandTests.cs @@ -0,0 +1,268 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Threading.Tasks; +using FirebirdSql.Data.TestsBase; +using NUnit.Framework; + +namespace FirebirdSql.Data.FirebirdClient.Tests +{ + [TestFixtureSource(typeof(FbServerTypeTestFixtureSource), nameof(FbServerTypeTestFixtureSource.Default))] + public class FbBatchCommandTests : FbTestsBase + { + bool _shouldTearDown; + + public FbBatchCommandTests(FbServerType serverType, bool compression, FbWireCrypt wireCrypt) + : base(serverType, compression, wireCrypt, false) + { + _shouldTearDown = false; + } + + [SetUp] + public override async Task SetUp() + { + await base.SetUp(); + + if (!EnsureServerVersion(new Version(4, 0, 0, 0))) + return; + + _shouldTearDown = true; + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "create table batch (i int check (i < 1000), i2 int128, ts timestamp)"; + await cmd.ExecuteNonQueryAsync(); + } + } + + [TearDown] + public override async Task TearDown() + { + if (_shouldTearDown) + { + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "drop table batch"; + await cmd.ExecuteNonQueryAsync(); + } + } + await base.TearDown(); + } + + [Test] + public async Task DataProperlyInDatabase() + { + await Empty(); + + var @is = new[] { -1, 6 }; + var bs = new[] { new BigInteger(long.MaxValue) * 2, new BigInteger(long.MaxValue) * 3 }; + var ts = new[] { new DateTime(2022, 01, 17, 1, 0, 0), new DateTime(2022, 01, 17, 2, 0, 0) }; + + await using (var cmd = Connection.CreateBatchCommand()) + { + cmd.CommandText = "insert into batch values (@i, @i2, @ts)"; + var batch1 = cmd.AddBatchParameters(); + batch1.Add("i", @is[0]); + batch1.Add("i2", bs[0]); + batch1.Add("ts", ts[0]); + var batch2 = cmd.AddBatchParameters(); + batch2.Add("i", @is[1]); + batch2.Add("i2", bs[1]); + batch2.Add("ts", ts[1]); + var result = await cmd.ExecuteNonQueryAsync(); + } + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "select i, i2, ts from batch order by i"; + using (var reader = await cmd.ExecuteReaderAsync()) + { + var index = 0; + while (await reader.ReadAsync()) + { + Assert.AreEqual(@is[index], reader[0]); + Assert.AreEqual(bs[index], reader[1]); + Assert.AreEqual(ts[index], reader[2]); + index += 1; + } + } + } + } + + [Test] + public async Task SuccessWithRecordsAffected() + { + await Empty(); + + await using (var cmd = Connection.CreateBatchCommand()) + { + cmd.CommandText = "insert into batch (i) values (@i)"; + var batch1 = cmd.AddBatchParameters(); + batch1.Add("i", 1); + var batch2 = cmd.AddBatchParameters(); + batch2.Add("i", 2); + var result = await cmd.ExecuteNonQueryAsync(); + + Assert.AreEqual(2, result.Count); + Assert.IsTrue(result.AllSuccess); + Assert.IsTrue(result[0].IsSuccess); + Assert.IsNull(result[0].Exception); + Assert.AreEqual(1, result[0].RecordsAffected); + Assert.IsTrue(result[1].IsSuccess); + Assert.IsNull(result[1].Exception); + Assert.AreEqual(1, result[1].RecordsAffected); + } + } + + [Test] + public async Task SuccessWithoutRecordsAffected() + { + await Empty(); + + var csb = BuildConnectionStringBuilder(ServerType, Compression, WireCrypt); + csb.ReturnRecordsAffected = false; + + await using (var conn = new FbConnection(csb.ToString())) + { + await conn.OpenAsync(); + await using (var cmd = conn.CreateBatchCommand()) + { + cmd.CommandText = "insert into batch (i) values (@i)"; + var batch1 = cmd.AddBatchParameters(); + batch1.Add("i", 1); + var batch2 = cmd.AddBatchParameters(); + batch2.Add("i", 2); + var result = await cmd.ExecuteNonQueryAsync(); + + Assert.AreEqual(2, result.Count); + Assert.IsTrue(result.AllSuccess); + Assert.IsTrue(result[0].IsSuccess); + Assert.IsNull(result[0].Exception); + Assert.AreEqual(-1, result[0].RecordsAffected); + Assert.IsTrue(result[1].IsSuccess); + Assert.IsNull(result[1].Exception); + Assert.AreEqual(-1, result[1].RecordsAffected); + } + } + } + + [Test] + public async Task ErrorWithoutMultiError() + { + await Empty(); + + await using (var cmd = Connection.CreateBatchCommand()) + { + cmd.MultiError = false; + + cmd.CommandText = "insert into batch (i) values (@i)"; + var batch1 = cmd.AddBatchParameters(); + batch1.Add("i", 1); + var batch2 = cmd.AddBatchParameters(); + batch2.Add("i", 1200); + var batch3 = cmd.AddBatchParameters(); + batch3.Add("i", 1300); + var result = await cmd.ExecuteNonQueryAsync(); + + Assert.AreEqual(2, result.Count); + Assert.IsFalse(result.AllSuccess); + Assert.IsTrue(result[0].IsSuccess); + Assert.IsNull(result[0].Exception); + Assert.AreEqual(1, result[0].RecordsAffected); + Assert.IsFalse(result[1].IsSuccess); + Assert.IsInstanceOf(result[1].Exception); + Assert.AreEqual(-1, result[1].RecordsAffected); + } + } + + [Test] + public async Task ErrorWithMultiError() + { + await Empty(); + + await using (var cmd = Connection.CreateBatchCommand()) + { + cmd.MultiError = true; + + cmd.CommandText = "insert into batch (i) values (@i)"; + var batch1 = cmd.AddBatchParameters(); + batch1.Add("i", 1); + var batch2 = cmd.AddBatchParameters(); + batch2.Add("i", 1200); + var batch3 = cmd.AddBatchParameters(); + batch3.Add("i", 1300); + var result = await cmd.ExecuteNonQueryAsync(); + + Assert.AreEqual(3, result.Count); + Assert.IsFalse(result.AllSuccess); + Assert.IsTrue(result[0].IsSuccess); + Assert.IsNull(result[0].Exception); + Assert.AreEqual(1, result[0].RecordsAffected); + Assert.IsFalse(result[1].IsSuccess); + Assert.IsInstanceOf(result[1].Exception); + Assert.AreEqual(-1, result[1].RecordsAffected); + Assert.IsFalse(result[2].IsSuccess); + Assert.IsInstanceOf(result[2].Exception); + Assert.AreEqual(-1, result[2].RecordsAffected); + } + } + + [Test] + [Ignore("Server bug (#7099).")] + public async Task ErrorWithMultiErrorWithOverflow() + { + await Empty(); + + await using (var cmd = Connection.CreateBatchCommand()) + { + cmd.MultiError = true; + + cmd.CommandText = "insert into batch (i) values (@i)"; + var batch1 = cmd.AddBatchParameters(); + batch1.Add("i", 1); + for (var i = 0; i < 100; i++) + { + var b = cmd.AddBatchParameters(); + b.Add("i", 1200); + } + var result = await cmd.ExecuteNonQueryAsync(); + + Assert.AreEqual(3, result.Count); + Assert.IsFalse(result.AllSuccess); + Assert.IsTrue(result[0].IsSuccess); + Assert.IsNull(result[0].Exception); + Assert.AreEqual(1, result[0].RecordsAffected); + Assert.IsFalse(result[1].IsSuccess); + Assert.IsInstanceOf(result[1].Exception); + Assert.AreEqual(-1, result[1].RecordsAffected); + Assert.IsFalse(result[2].IsSuccess); + Assert.IsInstanceOf(result[2].Exception); + Assert.AreEqual(-1, result[2].RecordsAffected); + } + } + + async Task Empty() + { + await using (var cmd = Connection.CreateCommand()) + { + cmd.CommandText = "delete from batch"; + await cmd.ExecuteNonQueryAsync(); + } + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbBooleanSupportTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbBooleanSupportTests.cs index 972b35ed..af97add1 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbBooleanSupportTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbBooleanSupportTests.cs @@ -16,7 +16,6 @@ //$Authors = Jiri Cincura (jiri@cincura.net) using System; -using System.Collections.Generic; using System.Threading.Tasks; using FirebirdSql.Data.TestsBase; using NUnit.Framework; @@ -49,17 +48,17 @@ await using (var cmd = Connection.CreateCommand()) cmd.CommandText = "CREATE TABLE withboolean (id INTEGER, bool BOOLEAN)"; await cmd.ExecuteNonQueryAsync(); } - var data = new Dictionary() + var data = new (int, string)[] { - { 0, "FALSE" }, - { 1, "TRUE" }, - { 2, "UNKNOWN" }, + (0, "FALSE"), + (1, "TRUE"), + (2, "UNKNOWN"), }; foreach (var item in data) { await using (var cmd = Connection.CreateCommand()) { - cmd.CommandText = $"INSERT INTO withboolean (id, bool) VALUES ({item.Key}, {item.Value})"; + cmd.CommandText = $"INSERT INTO withboolean (id, bool) VALUES ({item.Item1}, {item.Item2})"; await cmd.ExecuteNonQueryAsync(); } } diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs index c6652c43..7fb0f9e2 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat16SupportTests.cs @@ -29,7 +29,7 @@ namespace FirebirdSql.Data.FirebirdClient.Tests public class FbDecFloat16SupportTests : FbTestsBase { public FbDecFloat16SupportTests(FbServerType serverType, bool compression, FbWireCrypt wireCrypt) - : base(serverType, compression, wireCrypt) + : base(serverType, compression, wireCrypt, false) { } [SetUp] diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs index 304957b9..455ff265 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDecFloat34SupportTests.cs @@ -29,7 +29,7 @@ namespace FirebirdSql.Data.FirebirdClient.Tests public class FbDecFloat34SupportTests : FbTestsBase { public FbDecFloat34SupportTests(FbServerType serverType, bool compression, FbWireCrypt wireCrypt) - : base(serverType, compression, wireCrypt) + : base(serverType, compression, wireCrypt, false) { } [SetUp] diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbInt128SupportTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbInt128SupportTests.cs index a7b10196..b69eb00f 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbInt128SupportTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbInt128SupportTests.cs @@ -29,7 +29,7 @@ namespace FirebirdSql.Data.FirebirdClient.Tests public class FbInt128SupportTests : FbTestsBase { public FbInt128SupportTests(FbServerType serverType, bool compression, FbWireCrypt wireCrypt) - : base(serverType, compression, wireCrypt) + : base(serverType, compression, wireCrypt, false) { } [SetUp] diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbTimeZonesSupportTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbTimeZonesSupportTests.cs index c1a78171..8f21e58e 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbTimeZonesSupportTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbTimeZonesSupportTests.cs @@ -28,7 +28,7 @@ namespace FirebirdSql.Data.FirebirdClient.Tests public class FbTimeZonesSupportTests : FbTestsBase { public FbTimeZonesSupportTests(FbServerType serverType, bool compression, FbWireCrypt wireCrypt) - : base(serverType, compression, wireCrypt) + : base(serverType, compression, wireCrypt, false) { } [SetUp] diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/GdsConnection.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/GdsConnection.cs index ad5b2d5e..c518ca71 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/GdsConnection.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/GdsConnection.cs @@ -16,6 +16,7 @@ //$Authors = Carlos Guzman Alvarez, Jiri Cincura (jiri@cincura.net) using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -430,6 +431,40 @@ internal IResponse ProcessOperation(int operation) Xdr.ReadBoolean(), Xdr.ReadBuffer()); + case IscCodes.op_batch_cs: + var statementHandle = Xdr.ReadInt16(); + var p_batch_reccount = Xdr.ReadInt32(); + var p_batch_updates = Xdr.ReadInt32(); + var p_batch_vectors = Xdr.ReadInt32(); + var p_batch_errors = Xdr.ReadInt32(); + + var p_batch_updates_data = new int[p_batch_updates]; + for (var i = 0; i < p_batch_updates; i++) + { + p_batch_updates_data[i] = Xdr.ReadInt32(); + } + + var p_batch_vectors_data = new (int messageNumber, IscException statusVector)[p_batch_vectors]; + for (var i = 0; i < p_batch_vectors; i++) + { + var messageNumber = Xdr.ReadInt32(); + var statusVector = Xdr.ReadStatusVector(); + p_batch_vectors_data[i] = (messageNumber, statusVector); + } + + var p_batch_errors_data = new int[p_batch_errors]; + for (var i = 0; i < p_batch_errors; i++) + { + p_batch_errors_data[i] = Xdr.ReadInt32(); + } + + return new Version16.BatchCompletionStateResponse( + statementHandle, + p_batch_reccount, + p_batch_updates_data, + p_batch_vectors_data, + p_batch_errors_data); + default: throw new ArgumentOutOfRangeException(nameof(operation), $"{nameof(operation)}={operation}"); } @@ -463,13 +498,13 @@ internal async ValueTask ProcessOperationAsync(int operation, Cancell || ProtocolVersion == IscCodes.PROTOCOL_VERSION16) { return new Version15.CryptKeyCallbackResponse( - await Xdr.ReadBufferAsync().ConfigureAwait(false), - await Xdr.ReadInt32Async().ConfigureAwait(false)); + await Xdr.ReadBufferAsync(cancellationToken).ConfigureAwait(false), + await Xdr.ReadInt32Async(cancellationToken).ConfigureAwait(false)); } else { return new Version13.CryptKeyCallbackResponse( - await Xdr.ReadBufferAsync().ConfigureAwait(false)); + await Xdr.ReadBufferAsync(cancellationToken).ConfigureAwait(false)); } case IscCodes.op_cont_auth: @@ -479,6 +514,40 @@ internal async ValueTask ProcessOperationAsync(int operation, Cancell await Xdr.ReadBooleanAsync(cancellationToken).ConfigureAwait(false), await Xdr.ReadBufferAsync(cancellationToken).ConfigureAwait(false)); + case IscCodes.op_batch_cs: + var statementHandle = await Xdr.ReadInt16Async().ConfigureAwait(false); + var p_batch_reccount = await Xdr.ReadInt32Async().ConfigureAwait(false); + var p_batch_updates = await Xdr.ReadInt32Async().ConfigureAwait(false); + var p_batch_vectors = await Xdr.ReadInt32Async().ConfigureAwait(false); + var p_batch_errors = await Xdr.ReadInt32Async().ConfigureAwait(false); + + var p_batch_updates_data = new int[p_batch_updates]; + for (var i = 0; i < p_batch_updates; i++) + { + p_batch_updates_data[i] = await Xdr.ReadInt32Async().ConfigureAwait(false); + } + + var p_batch_vectors_data = new (int messageNumber, IscException statusVector)[p_batch_vectors]; + for (var i = 0; i < p_batch_vectors; i++) + { + var messageNumber = await Xdr.ReadInt32Async().ConfigureAwait(false); + var statusVector = await Xdr.ReadStatusVectorAsync().ConfigureAwait(false); + p_batch_vectors_data[i] = (messageNumber, statusVector); + } + + var p_batch_errors_data = new int[p_batch_errors]; + for (var i = 0; i < p_batch_errors; i++) + { + p_batch_errors_data[i] = await Xdr.ReadInt32Async().ConfigureAwait(false); + } + + return new Version16.BatchCompletionStateResponse( + statementHandle, + p_batch_reccount, + p_batch_updates_data, + p_batch_vectors_data, + p_batch_errors_data); + default: throw new ArgumentOutOfRangeException(nameof(operation), $"{nameof(operation)}={operation}"); } diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsDatabase.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsDatabase.cs index 5d5dcdcf..2e7b9173 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsDatabase.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsDatabase.cs @@ -822,6 +822,33 @@ public virtual async ValueTask ReadResponseAsync(int operation, Cance return response; } + public void SafeFinishFetching(int numberOfResponses) + { + while (numberOfResponses > 0) + { + numberOfResponses--; + try + { + ReadResponse(); + } + catch (IscException) + { } + } + } + public async ValueTask SafeFinishFetchingAsync(int numberOfResponses, CancellationToken cancellationToken = default) + { + while (numberOfResponses > 0) + { + numberOfResponses--; + try + { + await ReadResponseAsync(cancellationToken).ConfigureAwait(false); + } + catch (IscException) + { } + } + } + #endregion #region Protected Methods diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs index d1c7278c..4b59f9b8 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsStatement.cs @@ -92,6 +92,11 @@ public override int FetchSize set { _fetchSize = value; } } + public int Handle + { + get { return _handle; } + } + #endregion #region Constructors @@ -225,6 +230,15 @@ public override async ValueTask CreateArrayAsync(long handle, string #endregion + #region Batch Creation Methods + + public override BatchBase CreateBatch() + { + throw new NotSupportedException("Batching is not supported on this Firebird version."); + } + + #endregion + #region Methods public override void Prepare(string commandText) @@ -286,7 +300,7 @@ public override async ValueTask PrepareAsync(string commandText, CancellationTok } } - public override void Execute(int timeout) + public override void Execute(int timeout, IDescriptorFiller descriptorFiller) { EnsureNotDeallocated(); @@ -294,7 +308,7 @@ public override void Execute(int timeout) try { - SendExecuteToBuffer(timeout); + SendExecuteToBuffer(timeout, descriptorFiller); _database.Xdr.Flush(); @@ -325,7 +339,7 @@ public override void Execute(int timeout) throw IscException.ForIOException(ex); } } - public override async ValueTask ExecuteAsync(int timeout, CancellationToken cancellationToken = default) + public override async ValueTask ExecuteAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) { EnsureNotDeallocated(); @@ -333,7 +347,7 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc try { - await SendExecuteToBufferAsync(timeout, cancellationToken).ConfigureAwait(false); + await SendExecuteToBufferAsync(timeout, descriptorFiller, cancellationToken).ConfigureAwait(false); await _database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -389,7 +403,7 @@ public override DbValue[] Fetch() { _database.Xdr.Write(IscCodes.op_fetch); _database.Xdr.Write(_handle); - _database.Xdr.WriteBuffer(_fields.ToBlrArray()); + _database.Xdr.WriteBuffer(_fields.ToBlr().Data); _database.Xdr.Write(0); // p_sqldata_message_number _database.Xdr.Write(_fetchSize); // p_sqldata_messages _database.Xdr.Flush(); @@ -470,7 +484,7 @@ public override async ValueTask FetchAsync(CancellationToken cancella { await _database.Xdr.WriteAsync(IscCodes.op_fetch, cancellationToken).ConfigureAwait(false); await _database.Xdr.WriteAsync(_handle, cancellationToken).ConfigureAwait(false); - await _database.Xdr.WriteBufferAsync(_fields.ToBlrArray(), cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteBufferAsync(_fields.ToBlr().Data, cancellationToken).ConfigureAwait(false); await _database.Xdr.WriteAsync(0, cancellationToken).ConfigureAwait(false); // p_sqldata_message_number await _database.Xdr.WriteAsync(_fetchSize, cancellationToken).ConfigureAwait(false); // p_sqldata_messages await _database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -528,26 +542,6 @@ public override async ValueTask FetchAsync(CancellationToken cancella } } - public override void Describe() - { - // Nothing for Gds, because it's pre-fetched in Prepare. - } - public override ValueTask DescribeAsync(CancellationToken cancellationToken = default) - { - // Nothing for Gds, because it's pre-fetched in Prepare. - return ValueTask2.CompletedTask; - } - - public override void DescribeParameters() - { - // Nothing for Gds, because it's pre-fetched in Prepare. - } - public override ValueTask DescribeParametersAsync(CancellationToken cancellationToken = default) - { - // Nothing for Gds, because it's pre-fetched in Prepare. - return ValueTask2.CompletedTask; - } - #endregion #region Protected Methods @@ -772,10 +766,11 @@ protected ValueTask ProcessAllocateResponseAsync(GenericResponse response, Cance #endregion #region op_execute/op_execute2 methods - protected virtual void SendExecuteToBuffer(int timeout) + protected virtual void SendExecuteToBuffer(int timeout, IDescriptorFiller descriptorFiller) { + descriptorFiller.Fill(_parameters, 0); // this may throw error, so it needs to be before any writing - var descriptor = WriteParameters(); + var parametersData = WriteParameters(); if (StatementType == DbStatementType.StoredProcedure) { @@ -791,10 +786,10 @@ protected virtual void SendExecuteToBuffer(int timeout) if (_parameters != null) { - _database.Xdr.WriteBuffer(_parameters.ToBlrArray()); + _database.Xdr.WriteBuffer(_parameters.ToBlr().Data); _database.Xdr.Write(0); // Message number _database.Xdr.Write(1); // Number of messages - _database.Xdr.WriteBytes(descriptor, descriptor.Length); + _database.Xdr.WriteBytes(parametersData, parametersData.Length); } else { @@ -805,14 +800,15 @@ protected virtual void SendExecuteToBuffer(int timeout) if (StatementType == DbStatementType.StoredProcedure) { - _database.Xdr.WriteBuffer(_fields?.ToBlrArray()); + _database.Xdr.WriteBuffer(_fields?.ToBlr().Data); _database.Xdr.Write(0); // Output message number } } - protected virtual async ValueTask SendExecuteToBufferAsync(int timeout, CancellationToken cancellationToken = default) + protected virtual async ValueTask SendExecuteToBufferAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) { + await descriptorFiller.FillAsync(_parameters, 0, cancellationToken).ConfigureAwait(false); // this may throw error, so it needs to be before any writing - var descriptor = await WriteParametersAsync(cancellationToken).ConfigureAwait(false); + var parametersData = await WriteParametersAsync(cancellationToken).ConfigureAwait(false); if (StatementType == DbStatementType.StoredProcedure) { @@ -828,10 +824,10 @@ protected virtual async ValueTask SendExecuteToBufferAsync(int timeout, Cancella if (_parameters != null) { - await _database.Xdr.WriteBufferAsync(_parameters.ToBlrArray(), cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteBufferAsync(_parameters.ToBlr().Data, cancellationToken).ConfigureAwait(false); await _database.Xdr.WriteAsync(0, cancellationToken).ConfigureAwait(false); // Message number await _database.Xdr.WriteAsync(1, cancellationToken).ConfigureAwait(false); // Number of messages - await _database.Xdr.WriteBytesAsync(descriptor, descriptor.Length, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteBytesAsync(parametersData, parametersData.Length, cancellationToken).ConfigureAwait(false); } else { @@ -842,7 +838,7 @@ protected virtual async ValueTask SendExecuteToBufferAsync(int timeout, Cancella if (StatementType == DbStatementType.StoredProcedure) { - await _database.Xdr.WriteBufferAsync(_fields?.ToBlrArray(), cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteBufferAsync(_fields?.ToBlr().Data, cancellationToken).ConfigureAwait(false); await _database.Xdr.WriteAsync(0, cancellationToken).ConfigureAwait(false); // Output message number } } @@ -1694,57 +1690,6 @@ protected void ClearAll() _fields = null; } - protected virtual byte[] WriteParameters() - { - if (_parameters == null) - return null; - - using (var ms = new MemoryStream()) - { - var xdr = new XdrReaderWriter(new DataProviderStreamWrapper(ms), _database.Charset); - for (var i = 0; i < _parameters.Count; i++) - { - var field = _parameters[i]; - try - { - WriteRawParameter(xdr, field); - xdr.Write(field.NullFlag); - } - catch (IOException ex) - { - throw IscException.ForIOException(ex); - } - } - xdr.Flush(); - return ms.ToArray(); - } - } - protected virtual async ValueTask WriteParametersAsync(CancellationToken cancellationToken = default) - { - if (_parameters == null) - return null; - - using (var ms = new MemoryStream()) - { - var xdr = new XdrReaderWriter(new DataProviderStreamWrapper(ms), _database.Charset); - for (var i = 0; i < _parameters.Count; i++) - { - var field = _parameters[i]; - try - { - await WriteRawParameterAsync(xdr, field, cancellationToken).ConfigureAwait(false); - await xdr.WriteAsync(field.NullFlag, cancellationToken).ConfigureAwait(false); - } - catch (IOException ex) - { - throw IscException.ForIOException(ex); - } - } - await xdr.FlushAsync(cancellationToken).ConfigureAwait(false); - return ms.ToArray(); - } - } - protected virtual DbValue[] ReadRow() { var row = new DbValue[_fields.Count]; @@ -1805,5 +1750,60 @@ protected virtual async ValueTask ReadRowAsync(CancellationToken canc } #endregion + + #region Protected Internal Methods + + protected internal virtual byte[] WriteParameters() + { + if (_parameters == null) + return null; + + using (var ms = new MemoryStream()) + { + var xdr = new XdrReaderWriter(new DataProviderStreamWrapper(ms), _database.Charset); + for (var i = 0; i < _parameters.Count; i++) + { + var field = _parameters[i]; + try + { + WriteRawParameter(xdr, field); + xdr.Write(field.NullFlag); + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + } + xdr.Flush(); + return ms.ToArray(); + } + } + protected internal virtual async ValueTask WriteParametersAsync(CancellationToken cancellationToken = default) + { + if (_parameters == null) + return null; + + using (var ms = new MemoryStream()) + { + var xdr = new XdrReaderWriter(new DataProviderStreamWrapper(ms), _database.Charset); + for (var i = 0; i < _parameters.Count; i++) + { + var field = _parameters[i]; + try + { + await WriteRawParameterAsync(xdr, field, cancellationToken).ConfigureAwait(false); + await xdr.WriteAsync(field.NullFlag, cancellationToken).ConfigureAwait(false); + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + } + await xdr.FlushAsync(cancellationToken).ConfigureAwait(false); + return ms.ToArray(); + } + } + + #endregion } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version11/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version11/GdsStatement.cs index 52e04944..91da7150 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version11/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version11/GdsStatement.cs @@ -15,6 +15,7 @@ //$Authors = Carlos Guzman Alvarez, Jiri Cincura (jiri@cincura.net) +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -85,7 +86,7 @@ public override void Prepare(string commandText) } finally { - numberOfResponses = SafeFinishFetching(numberOfResponses); + (Database as GdsDatabase).SafeFinishFetching(numberOfResponses); } State = StatementState.Prepared; @@ -142,7 +143,7 @@ public override async ValueTask PrepareAsync(string commandText, CancellationTok } finally { - numberOfResponses = await SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); + await (Database as GdsDatabase).SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); } State = StatementState.Prepared; @@ -154,7 +155,7 @@ public override async ValueTask PrepareAsync(string commandText, CancellationTok } } - public override void Execute(int timeout) + public override void Execute(int timeout, IDescriptorFiller descriptorFiller) { EnsureNotDeallocated(); @@ -164,7 +165,7 @@ public override void Execute(int timeout) { RecordsAffected = -1; - SendExecuteToBuffer(timeout); + SendExecuteToBuffer(timeout, descriptorFiller); var readRowsAffectedResponse = false; if (DoRecordsAffected) @@ -205,7 +206,7 @@ public override void Execute(int timeout) } finally { - numberOfResponses = SafeFinishFetching(numberOfResponses); + (Database as GdsDatabase).SafeFinishFetching(numberOfResponses); } State = StatementState.Executed; @@ -216,7 +217,7 @@ public override void Execute(int timeout) throw IscException.ForIOException(ex); } } - public override async ValueTask ExecuteAsync(int timeout, CancellationToken cancellationToken = default) + public override async ValueTask ExecuteAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) { EnsureNotDeallocated(); @@ -226,7 +227,7 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc { RecordsAffected = -1; - await SendExecuteToBufferAsync(timeout, cancellationToken).ConfigureAwait(false); + await SendExecuteToBufferAsync(timeout, descriptorFiller, cancellationToken).ConfigureAwait(false); var readRowsAffectedResponse = false; if (DoRecordsAffected) @@ -267,7 +268,7 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc } finally { - numberOfResponses = await SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); + await (Database as GdsDatabase).SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); } State = StatementState.Executed; @@ -282,35 +283,6 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc #endregion #region Protected methods - protected int SafeFinishFetching(int numberOfResponses) - { - while (numberOfResponses > 0) - { - numberOfResponses--; - try - { - _database.ReadResponse(); - } - catch (IscException) - { } - } - return numberOfResponses; - } - protected async ValueTask SafeFinishFetchingAsync(int numberOfResponses, CancellationToken cancellationToken = default) - { - while (numberOfResponses > 0) - { - numberOfResponses--; - try - { - await _database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); - } - catch (IscException) - { } - } - return numberOfResponses; - } - protected override void Free(int option) { if (FreeNotNeeded(option)) diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version12/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version12/GdsStatement.cs index ec419768..f9666f16 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version12/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version12/GdsStatement.cs @@ -40,7 +40,7 @@ public GdsStatement(DatabaseBase db, TransactionBase transaction) #region Overriden Methods - public override void Execute(int timeout) + public override void Execute(int timeout, IDescriptorFiller descriptorFiller) { EnsureNotDeallocated(); @@ -50,7 +50,7 @@ public override void Execute(int timeout) { RecordsAffected = -1; - SendExecuteToBuffer(timeout); + SendExecuteToBuffer(timeout, descriptorFiller); _database.Xdr.Flush(); @@ -71,7 +71,7 @@ public override void Execute(int timeout) } finally { - numberOfResponses = SafeFinishFetching(numberOfResponses); + (Database as GdsDatabase).SafeFinishFetching(numberOfResponses); } // we need to split this in two, to allow server handle op_cancel properly @@ -91,7 +91,7 @@ public override void Execute(int timeout) } finally { - numberOfResponses = SafeFinishFetching(numberOfResponses); + (Database as GdsDatabase).SafeFinishFetching(numberOfResponses); } } @@ -103,7 +103,7 @@ public override void Execute(int timeout) throw IscException.ForIOException(ex); } } - public override async ValueTask ExecuteAsync(int timeout, CancellationToken cancellationToken = default) + public override async ValueTask ExecuteAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) { EnsureNotDeallocated(); @@ -113,7 +113,7 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc { RecordsAffected = -1; - await SendExecuteToBufferAsync(timeout, cancellationToken).ConfigureAwait(false); + await SendExecuteToBufferAsync(timeout, descriptorFiller, cancellationToken).ConfigureAwait(false); await _database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); @@ -134,7 +134,7 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc } finally { - numberOfResponses = await SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); + await (Database as GdsDatabase).SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); } // we need to split this in two, to allow server handle op_cancel properly @@ -154,7 +154,7 @@ public override async ValueTask ExecuteAsync(int timeout, CancellationToken canc } finally { - numberOfResponses = await SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); + await (Database as GdsDatabase).SafeFinishFetchingAsync(numberOfResponses, cancellationToken).ConfigureAwait(false); } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version13/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version13/GdsStatement.cs index 7e8be2a9..8e06969e 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version13/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version13/GdsStatement.cs @@ -40,7 +40,66 @@ public GdsStatement(DatabaseBase db, TransactionBase transaction) #region Overriden Methods - protected override byte[] WriteParameters() + protected override DbValue[] ReadRow() + { + var row = new DbValue[_fields.Count]; + try + { + if (_fields.Count > 0) + { + var nullBytes = _database.Xdr.ReadOpaque((int)Math.Ceiling(_fields.Count / 8d)); + var nullBits = new BitArray(nullBytes); + for (var i = 0; i < _fields.Count; i++) + { + if (nullBits.Get(i)) + { + row[i] = new DbValue(this, _fields[i], null); + } + else + { + var value = ReadRawValue(_database.Xdr, _fields[i]); + row[i] = new DbValue(this, _fields[i], value); + } + } + } + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + return row; + } + protected override async ValueTask ReadRowAsync(CancellationToken cancellationToken = default) + { + var row = new DbValue[_fields.Count]; + try + { + if (_fields.Count > 0) + { + var nullBytes = await _database.Xdr.ReadOpaqueAsync((int)Math.Ceiling(_fields.Count / 8d), cancellationToken).ConfigureAwait(false); + var nullBits = new BitArray(nullBytes); + for (var i = 0; i < _fields.Count; i++) + { + if (nullBits.Get(i)) + { + row[i] = new DbValue(this, _fields[i], null); + } + else + { + var value = await ReadRawValueAsync(_database.Xdr, _fields[i], cancellationToken).ConfigureAwait(false); + row[i] = new DbValue(this, _fields[i], value); + } + } + } + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + return row; + } + + protected internal override byte[] WriteParameters() { if (_parameters == null) return null; @@ -85,7 +144,7 @@ protected override byte[] WriteParameters() } } } - protected override async ValueTask WriteParametersAsync(CancellationToken cancellationToken = default) + protected internal override async ValueTask WriteParametersAsync(CancellationToken cancellationToken = default) { if (_parameters == null) return null; @@ -131,65 +190,6 @@ protected override async ValueTask WriteParametersAsync(CancellationToke } } - protected override DbValue[] ReadRow() - { - var row = new DbValue[_fields.Count]; - try - { - if (_fields.Count > 0) - { - var nullBytes = _database.Xdr.ReadOpaque((int)Math.Ceiling(_fields.Count / 8d)); - var nullBits = new BitArray(nullBytes); - for (var i = 0; i < _fields.Count; i++) - { - if (nullBits.Get(i)) - { - row[i] = new DbValue(this, _fields[i], null); - } - else - { - var value = ReadRawValue(_database.Xdr, _fields[i]); - row[i] = new DbValue(this, _fields[i], value); - } - } - } - } - catch (IOException ex) - { - throw IscException.ForIOException(ex); - } - return row; - } - protected override async ValueTask ReadRowAsync(CancellationToken cancellationToken = default) - { - var row = new DbValue[_fields.Count]; - try - { - if (_fields.Count > 0) - { - var nullBytes = await _database.Xdr.ReadOpaqueAsync((int)Math.Ceiling(_fields.Count / 8d), cancellationToken).ConfigureAwait(false); - var nullBits = new BitArray(nullBytes); - for (var i = 0; i < _fields.Count; i++) - { - if (nullBits.Get(i)) - { - row[i] = new DbValue(this, _fields[i], null); - } - else - { - var value = await ReadRawValueAsync(_database.Xdr, _fields[i], cancellationToken).ConfigureAwait(false); - row[i] = new DbValue(this, _fields[i], value); - } - } - } - } - catch (IOException ex) - { - throw IscException.ForIOException(ex); - } - return row; - } - #endregion } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/BatchCompletionStateResponse.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/BatchCompletionStateResponse.cs new file mode 100644 index 00000000..a818f802 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/BatchCompletionStateResponse.cs @@ -0,0 +1,39 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using FirebirdSql.Data.Common; + +namespace FirebirdSql.Data.Client.Managed.Version16 +{ + internal class BatchCompletionStateResponse : IResponse + { + public short StatementHandle { get; } + public int ProcessedMessages { get; } + public int[] UpdatedRecordsPerMessage { get; } + public (int, IscException)[] DetailedErrors { get; } + public int[] AdditionalErrorsPerMessage { get; } + + public BatchCompletionStateResponse(short statementHandle, int processedMessages, int[] updatedRecordsPerMessage, (int, IscException)[] detailedErrors, int[] errorsPerMessage) + { + StatementHandle = statementHandle; + ProcessedMessages = processedMessages; + UpdatedRecordsPerMessage = updatedRecordsPerMessage; + DetailedErrors = detailedErrors; + AdditionalErrorsPerMessage = errorsPerMessage; + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsBatch.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsBatch.cs new file mode 100644 index 00000000..cf7781f8 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsBatch.cs @@ -0,0 +1,216 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FirebirdSql.Data.Common; + +namespace FirebirdSql.Data.Client.Managed.Version16 +{ + internal class GdsBatch : BatchBase + { + protected GdsStatement _statement; + + public override StatementBase Statement => _statement; + + public GdsDatabase Database => (GdsDatabase)_statement.Database; + + public GdsBatch(GdsStatement statement) + { + _statement = statement; + } + + public override ExecuteResultItem[] Execute(int count, IDescriptorFiller descriptorFiller) + { + var parametersData = new byte[count][]; + for (var i = 0; i < parametersData.Length; i++) + { + descriptorFiller.Fill(_statement.Parameters, i); + // this may throw error, so it needs to be before any writing + parametersData[i] = _statement.WriteParameters(); + } + + Database.Xdr.Write(IscCodes.op_batch_create); + Database.Xdr.Write(_statement.Handle); // p_batch_statement + var blr = _statement.Parameters.ToBlr(); + Database.Xdr.WriteBuffer(blr.Data); // p_batch_blr + Database.Xdr.Write(blr.Length); // p_batch_msglen + var pb = new BatchParameterBuffer(); + if (_statement.ReturnRecordsAffected) + { + pb.Append(IscCodes.Batch.TAG_RECORD_COUNTS, 1); + } + if (MultiError) + { + pb.Append(IscCodes.Batch.TAG_MULTIERROR, 1); + } + Database.Xdr.WriteBuffer(pb.ToArray()); // p_batch_pb + + Database.Xdr.Write(IscCodes.op_batch_msg); + Database.Xdr.Write(_statement.Handle); // p_batch_statement + Database.Xdr.Write(parametersData.Length); // p_batch_messages + foreach (var item in parametersData) + { + Database.Xdr.WriteOpaque(item, item.Length); // p_batch_data + } + + Database.Xdr.Write(IscCodes.op_batch_exec); + Database.Xdr.Write(_statement.Handle); // p_batch_statement + Database.Xdr.Write(_statement.Transaction.Handle); // p_batch_transaction; + + Database.Xdr.Flush(); + + var numberOfResponses = 3; + try + { + numberOfResponses--; + var batchCreateResponse = Database.ReadResponse(); + numberOfResponses--; + var batchMsgResponse = Database.ReadResponse(); + numberOfResponses--; + var batchExecResponse = (BatchCompletionStateResponse)Database.ReadResponse(); + + return BuildResult(batchExecResponse); + } + finally + { + Database.SafeFinishFetching(numberOfResponses); + } + } + public override async ValueTask ExecuteAsync(int count, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) + { + var parametersData = new byte[count][]; + for (var i = 0; i < parametersData.Length; i++) + { + await descriptorFiller.FillAsync(_statement.Parameters, i, cancellationToken).ConfigureAwait(false); + // this may throw error, so it needs to be before any writing + parametersData[i] = await _statement.WriteParametersAsync(cancellationToken).ConfigureAwait(false); + } + + await Database.Xdr.WriteAsync(IscCodes.op_batch_create, cancellationToken).ConfigureAwait(false); + await Database.Xdr.WriteAsync(_statement.Handle, cancellationToken).ConfigureAwait(false); // p_batch_statement + var blr = _statement.Parameters.ToBlr(); + await Database.Xdr.WriteBufferAsync(blr.Data, cancellationToken).ConfigureAwait(false); // p_batch_blr + await Database.Xdr.WriteAsync(blr.Length, cancellationToken).ConfigureAwait(false); // p_batch_msglen + var pb = new BatchParameterBuffer(); + if (_statement.ReturnRecordsAffected) + { + pb.Append(IscCodes.Batch.TAG_RECORD_COUNTS, 1); + } + if (MultiError) + { + pb.Append(IscCodes.Batch.TAG_MULTIERROR, 1); + } + await Database.Xdr.WriteBufferAsync(pb.ToArray(), cancellationToken).ConfigureAwait(false); // p_batch_pb + + await Database.Xdr.WriteAsync(IscCodes.op_batch_msg, cancellationToken).ConfigureAwait(false); + await Database.Xdr.WriteAsync(_statement.Handle, cancellationToken).ConfigureAwait(false); // p_batch_statement + await Database.Xdr.WriteAsync(parametersData.Length, cancellationToken).ConfigureAwait(false); // p_batch_messages + foreach (var item in parametersData) + { + await Database.Xdr.WriteOpaqueAsync(item, item.Length, cancellationToken).ConfigureAwait(false); // p_batch_data + } + + await Database.Xdr.WriteAsync(IscCodes.op_batch_exec, cancellationToken).ConfigureAwait(false); + await Database.Xdr.WriteAsync(_statement.Handle, cancellationToken).ConfigureAwait(false); // p_batch_statement + await Database.Xdr.WriteAsync(_statement.Transaction.Handle, cancellationToken).ConfigureAwait(false); // p_batch_transaction; + + await Database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); + + var numberOfResponses = 3; + try + { + numberOfResponses--; + var batchCreateResponse = await Database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); + numberOfResponses--; + var batchMsgResponse = await Database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); + numberOfResponses--; + var batchExecResponse = (BatchCompletionStateResponse)await Database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); + + return BuildResult(batchExecResponse); + } + finally + { + await Database.SafeFinishFetchingAsync(numberOfResponses).ConfigureAwait(false); + } + } + + public override void Dispose2() + { + Database.Xdr.Write(IscCodes.op_batch_rls); + Database.Xdr.Write(_statement.Handle); + Database.AppendDeferredPacket(ProcessReleaseResponse); + } + + public override async ValueTask Dispose2Async(CancellationToken cancellationToken = default) + { + await Database.Xdr.WriteAsync(IscCodes.op_batch_rls, cancellationToken).ConfigureAwait(false); + await Database.Xdr.WriteAsync(_statement.Handle, cancellationToken).ConfigureAwait(false); + Database.AppendDeferredPacket(ProcessReleaseResponseAsync); + } + + protected void ProcessReleaseResponse(IResponse response) + { } + protected ValueTask ProcessReleaseResponseAsync(IResponse response, CancellationToken cancellationToken = default) + { + return ValueTask2.CompletedTask; + } + + protected ExecuteResultItem[] BuildResult(BatchCompletionStateResponse response) + { + var detailedErrors = response.DetailedErrors.ToDictionary(x => x.Item1, x => x.Item2); + var additionalErrorsPerMessage = response.AdditionalErrorsPerMessage.ToHashSet(); + var result = new ExecuteResultItem[response.ProcessedMessages]; + for (var i = 0; i < result.Length; i++) + { + var recordsAffected = i < response.UpdatedRecordsPerMessage.Length + ? response.UpdatedRecordsPerMessage[i] + : -1; + if (detailedErrors.TryGetValue(i, out var exception)) + { + result[i] = new ExecuteResultItem() + { + RecordsAffected = recordsAffected, + IsError = true, + Exception = exception, + }; + } + else if (additionalErrorsPerMessage.Contains(i)) + { + result[i] = new ExecuteResultItem() + { + RecordsAffected = recordsAffected, + IsError = true, + Exception = null, + }; + } + else + { + result[i] = new ExecuteResultItem() + { + RecordsAffected = recordsAffected, + IsError = false, + Exception = null, + }; + } + } + return result; + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsStatement.cs index 38ef03ad..ab634024 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version16/GdsStatement.cs @@ -16,8 +16,6 @@ //$Authors = Jiri Cincura (jiri@cincura.net) using System; -using System.Collections; -using System.IO; using System.Threading; using System.Threading.Tasks; using FirebirdSql.Data.Common; @@ -34,16 +32,21 @@ public GdsStatement(DatabaseBase db, TransactionBase transaction) : base(db, transaction) { } - protected override void SendExecuteToBuffer(int timeout) + protected override void SendExecuteToBuffer(int timeout, IDescriptorFiller descriptorFiller) { - base.SendExecuteToBuffer(timeout); + base.SendExecuteToBuffer(timeout, descriptorFiller); _database.Xdr.Write(timeout); } - protected override async ValueTask SendExecuteToBufferAsync(int timeout, CancellationToken cancellationToken = default) + protected override async ValueTask SendExecuteToBufferAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) { - await base.SendExecuteToBufferAsync(timeout, cancellationToken).ConfigureAwait(false); + await base.SendExecuteToBufferAsync(timeout, descriptorFiller, cancellationToken).ConfigureAwait(false); await _database.Xdr.WriteAsync(timeout, cancellationToken).ConfigureAwait(false); } + + public override BatchBase CreateBatch() + { + return new GdsBatch(this); + } } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs index 14ccc1fc..b99bb7a3 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesStatement.cs @@ -218,6 +218,12 @@ public override async ValueTask CreateArrayAsync(long handle, string return array; } + public override BatchBase CreateBatch() + { +#warning Check supported fbclient? + throw new NotSupportedException("Batching is not supported on this Firebird version."); + } + #endregion #region Methods @@ -297,6 +303,8 @@ public override void Prepare(string commandText) _fields.ResetValues(); + DescribeParameters(); + StatementType = GetStatementType(); State = StatementState.Prepared; @@ -338,7 +346,7 @@ public override async ValueTask PrepareAsync(string commandText, CancellationTok if (_fields.ActualCount > 0 && _fields.ActualCount != _fields.Count) { - await DescribeAsync(cancellationToken).ConfigureAwait(false); + Describe(); } else { @@ -350,15 +358,19 @@ public override async ValueTask PrepareAsync(string commandText, CancellationTok _fields.ResetValues(); + DescribeParameters(); + StatementType = await GetStatementTypeAsync(cancellationToken).ConfigureAwait(false); State = StatementState.Prepared; } - public override void Execute(int timeout) + public override void Execute(int timeout, IDescriptorFiller descriptorFiller) { EnsureNotDeallocated(); + descriptorFiller.Fill(_parameters, 0); + ClearStatusVector(); NativeHelpers.CallIfExists(() => { @@ -423,10 +435,12 @@ public override void Execute(int timeout) State = StatementState.Executed; } - public override async ValueTask ExecuteAsync(int timeout, CancellationToken cancellationToken = default) + public override async ValueTask ExecuteAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default) { EnsureNotDeallocated(); + await descriptorFiller.FillAsync(_parameters, 0, cancellationToken).ConfigureAwait(false); + ClearStatusVector(); NativeHelpers.CallIfExists(() => { @@ -633,168 +647,6 @@ public override async ValueTask FetchAsync(CancellationToken cancella } } - public override void Describe() - { - ClearStatusVector(); - - _fields = new Descriptor(_fields.ActualCount); - - var sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, _fields); - - - _db.FbClient.isc_dsql_describe( - _statusVector, - ref _handle, - IscCodes.SQLDA_VERSION1, - sqlda); - - var descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); - - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - - _db.ProcessStatusVector(_statusVector); - - _fields = descriptor; - } - public override ValueTask DescribeAsync(CancellationToken cancellationToken = default) - { - ClearStatusVector(); - - _fields = new Descriptor(_fields.ActualCount); - - var sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, _fields); - - - _db.FbClient.isc_dsql_describe( - _statusVector, - ref _handle, - IscCodes.SQLDA_VERSION1, - sqlda); - - var descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); - - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - - _db.ProcessStatusVector(_statusVector); - - _fields = descriptor; - - return ValueTask2.CompletedTask; - } - - public override void DescribeParameters() - { - ClearStatusVector(); - - _parameters = new Descriptor(1); - - var sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, _parameters); - - - _db.FbClient.isc_dsql_describe_bind( - _statusVector, - ref _handle, - IscCodes.SQLDA_VERSION1, - sqlda); - - var descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); - - _db.ProcessStatusVector(_statusVector); - - if (descriptor.ActualCount != 0 && descriptor.Count != descriptor.ActualCount) - { - var n = descriptor.ActualCount; - descriptor = new Descriptor(n); - - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - - sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, descriptor); - - _db.FbClient.isc_dsql_describe_bind( - _statusVector, - ref _handle, - IscCodes.SQLDA_VERSION1, - sqlda); - - descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); - - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - - _db.ProcessStatusVector(_statusVector); - } - else - { - if (descriptor.ActualCount == 0) - { - descriptor = new Descriptor(0); - } - } - - if (sqlda != IntPtr.Zero) - { - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - } - - _parameters = descriptor; - } - public override ValueTask DescribeParametersAsync(CancellationToken cancellationToken = default) - { - ClearStatusVector(); - - _parameters = new Descriptor(1); - - var sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, _parameters); - - - _db.FbClient.isc_dsql_describe_bind( - _statusVector, - ref _handle, - IscCodes.SQLDA_VERSION1, - sqlda); - - var descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); - - _db.ProcessStatusVector(_statusVector); - - if (descriptor.ActualCount != 0 && descriptor.Count != descriptor.ActualCount) - { - var n = descriptor.ActualCount; - descriptor = new Descriptor(n); - - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - - sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, descriptor); - - _db.FbClient.isc_dsql_describe_bind( - _statusVector, - ref _handle, - IscCodes.SQLDA_VERSION1, - sqlda); - - descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); - - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - - _db.ProcessStatusVector(_statusVector); - } - else - { - if (descriptor.ActualCount == 0) - { - descriptor = new Descriptor(0); - } - } - - if (sqlda != IntPtr.Zero) - { - XsqldaMarshaler.CleanUpNativeData(ref sqlda); - } - - _parameters = descriptor; - - return ValueTask2.CompletedTask; - } - #endregion #region Protected Methods @@ -945,6 +797,85 @@ private void Allocate() StatementType = DbStatementType.None; } + private void Describe() + { + ClearStatusVector(); + + _fields = new Descriptor(_fields.ActualCount); + + var sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, _fields); + + _db.FbClient.isc_dsql_describe( + _statusVector, + ref _handle, + IscCodes.SQLDA_VERSION1, + sqlda); + + var descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); + + XsqldaMarshaler.CleanUpNativeData(ref sqlda); + + _db.ProcessStatusVector(_statusVector); + + _fields = descriptor; + } + + private void DescribeParameters() + { + ClearStatusVector(); + + _parameters = new Descriptor(1); + + var sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, _parameters); + + + _db.FbClient.isc_dsql_describe_bind( + _statusVector, + ref _handle, + IscCodes.SQLDA_VERSION1, + sqlda); + + var descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); + + _db.ProcessStatusVector(_statusVector); + + if (descriptor.ActualCount != 0 && descriptor.Count != descriptor.ActualCount) + { + var n = descriptor.ActualCount; + descriptor = new Descriptor(n); + + XsqldaMarshaler.CleanUpNativeData(ref sqlda); + + sqlda = XsqldaMarshaler.MarshalManagedToNative(_db.Charset, descriptor); + + _db.FbClient.isc_dsql_describe_bind( + _statusVector, + ref _handle, + IscCodes.SQLDA_VERSION1, + sqlda); + + descriptor = XsqldaMarshaler.MarshalNativeToManaged(_db.Charset, sqlda); + + XsqldaMarshaler.CleanUpNativeData(ref sqlda); + + _db.ProcessStatusVector(_statusVector); + } + else + { + if (descriptor.ActualCount == 0) + { + descriptor = new Descriptor(0); + } + } + + if (sqlda != IntPtr.Zero) + { + XsqldaMarshaler.CleanUpNativeData(ref sqlda); + } + + _parameters = descriptor; + } + #endregion } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/ArrayBase.cs b/src/FirebirdSql.Data.FirebirdClient/Common/ArrayBase.cs index 68df09c2..98acdcf1 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/ArrayBase.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/ArrayBase.cs @@ -120,7 +120,7 @@ private void LookupBounds() try { lookup.Prepare(GetArrayBounds()); - lookup.Execute(0); + lookup.Execute(0, EmptyDescriptorFiller.Instance); _descriptor.Bounds = new ArrayBound[16]; DbValue[] values; @@ -146,7 +146,7 @@ private async ValueTask LookupBoundsAsync(CancellationToken cancellationToken = try { await lookup.PrepareAsync(GetArrayBounds(), cancellationToken).ConfigureAwait(false); - await lookup.ExecuteAsync(0, cancellationToken).ConfigureAwait(false); + await lookup.ExecuteAsync(0, EmptyDescriptorFiller.Instance, cancellationToken).ConfigureAwait(false); _descriptor.Bounds = new ArrayBound[16]; DbValue[] values; @@ -171,7 +171,7 @@ private void LookupDesc() try { lookup.Prepare(GetArrayDesc()); - lookup.Execute(0); + lookup.Execute(0, EmptyDescriptorFiller.Instance); _descriptor = new ArrayDesc(); var values = lookup.Fetch(); @@ -203,7 +203,7 @@ private async ValueTask LookupDescAsync(CancellationToken cancellationToken = de try { await lookup.PrepareAsync(GetArrayDesc(), cancellationToken).ConfigureAwait(false); - await lookup.ExecuteAsync(0, cancellationToken).ConfigureAwait(false); + await lookup.ExecuteAsync(0, EmptyDescriptorFiller.Instance, cancellationToken).ConfigureAwait(false); _descriptor = new ArrayDesc(); var values = await lookup.FetchAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/BatchBase.cs b/src/FirebirdSql.Data.FirebirdClient/Common/BatchBase.cs new file mode 100644 index 00000000..8d534693 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/BatchBase.cs @@ -0,0 +1,44 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System.Threading; +using System.Threading.Tasks; + +namespace FirebirdSql.Data.Common +{ + internal abstract class BatchBase + { + public abstract StatementBase Statement { get; } + public bool MultiError { get; set; } + + public class ExecuteResultItem + { + public int RecordsAffected { get; set; } + public bool IsError { get; set; } + public IscException Exception { get; set; } + } + public abstract ExecuteResultItem[] Execute(int count, IDescriptorFiller descriptorFiller); + public abstract ValueTask ExecuteAsync(int count, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default); + + public virtual void Dispose2() + { } + public virtual ValueTask Dispose2Async(CancellationToken cancellationToken = default) + { + return ValueTask2.CompletedTask; + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/BatchParameterBuffer.cs b/src/FirebirdSql.Data.FirebirdClient/Common/BatchParameterBuffer.cs new file mode 100644 index 00000000..fb1c4094 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/BatchParameterBuffer.cs @@ -0,0 +1,54 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +namespace FirebirdSql.Data.Common +{ + internal sealed class BatchParameterBuffer : DatabaseParameterBufferBase + { + public BatchParameterBuffer() + : base(IscCodes.Batch.VERSION1) + { } + + public override void Append(int type, byte value) + { + WriteByte(type); + Write(1); + Write(value); + } + + public override void Append(int type, short value) + { + WriteByte(type); + Write(2); + Write(value); + } + + public override void Append(int type, int value) + { + WriteByte(type); + Write(4); + Write(value); + } + + public override void Append(int type, byte[] buffer) + { + WriteByte(type); + Write(buffer.Length); + Write(buffer); + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/Descriptor.cs b/src/FirebirdSql.Data.FirebirdClient/Common/Descriptor.cs index a026a309..f390c720 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/Descriptor.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/Descriptor.cs @@ -42,14 +42,7 @@ internal sealed class Descriptor public short Version { - get - { - return _version; - } - set - { - _version = value; - } + get { return _version; } } public short Count @@ -76,7 +69,11 @@ public short ActualCount #region Constructors + private Descriptor() + { } + public Descriptor(short n) + : this() { _version = IscCodes.SQLDA_VERSION1; _count = n; @@ -101,16 +98,29 @@ public void ResetValues() } } - public byte[] ToBlrArray() + internal sealed class BlrData + { + public byte[] Data { get; } + public int Length { get; } + + public BlrData(byte[] data, int length) + { + Data = data; + Length = length; + } + } + public BlrData ToBlr() { using (var blr = new MemoryStream()) { - var par_count = Count * 2; +#warning Refactor handling of this length + var length = 0; blr.WriteByte(IscCodes.blr_version5); blr.WriteByte(IscCodes.blr_begin); blr.WriteByte(IscCodes.blr_message); blr.WriteByte(0); + var par_count = Count * 2; blr.WriteByte((byte)(par_count & 255)); blr.WriteByte((byte)(par_count >> 8)); @@ -125,116 +135,165 @@ public byte[] ToBlrArray() blr.WriteByte(IscCodes.blr_varying); blr.WriteByte((byte)(len & 255)); blr.WriteByte((byte)(len >> 8)); + length = TypeHelper.BlrAlign(length, 2); + length += len + 2; break; case IscCodes.SQL_TEXT: blr.WriteByte(IscCodes.blr_text); blr.WriteByte((byte)(len & 255)); blr.WriteByte((byte)(len >> 8)); + // no align + length += len; break; case IscCodes.SQL_DOUBLE: blr.WriteByte(IscCodes.blr_double); + length = TypeHelper.BlrAlign(length, 8); + length += 8; break; case IscCodes.SQL_FLOAT: blr.WriteByte(IscCodes.blr_float); + length = TypeHelper.BlrAlign(length, 4); + length += 4; break; case IscCodes.SQL_D_FLOAT: blr.WriteByte(IscCodes.blr_d_float); + length = TypeHelper.BlrAlign(length, 8); + length += 8; break; case IscCodes.SQL_TYPE_DATE: blr.WriteByte(IscCodes.blr_sql_date); + length = TypeHelper.BlrAlign(length, 4); + length += 4; break; case IscCodes.SQL_TYPE_TIME: blr.WriteByte(IscCodes.blr_sql_time); + length = TypeHelper.BlrAlign(length, 4); + length += 4; break; case IscCodes.SQL_TIMESTAMP: blr.WriteByte(IscCodes.blr_timestamp); + length = TypeHelper.BlrAlign(length, 4); + length += 8; break; case IscCodes.SQL_BLOB: blr.WriteByte(IscCodes.blr_quad); blr.WriteByte(0); + length = TypeHelper.BlrAlign(length, 4); + length += 8; break; case IscCodes.SQL_ARRAY: blr.WriteByte(IscCodes.blr_quad); blr.WriteByte(0); + length = TypeHelper.BlrAlign(length, 4); + length += 8; break; case IscCodes.SQL_LONG: blr.WriteByte(IscCodes.blr_long); blr.WriteByte((byte)_fields[i].NumericScale); + length = TypeHelper.BlrAlign(length, 4); + length += 4; break; case IscCodes.SQL_SHORT: blr.WriteByte(IscCodes.blr_short); blr.WriteByte((byte)_fields[i].NumericScale); + length = TypeHelper.BlrAlign(length, 2); + length += 2; break; case IscCodes.SQL_INT64: blr.WriteByte(IscCodes.blr_int64); blr.WriteByte((byte)_fields[i].NumericScale); + length = TypeHelper.BlrAlign(length, 8); + length += 8; break; case IscCodes.SQL_QUAD: blr.WriteByte(IscCodes.blr_quad); blr.WriteByte((byte)_fields[i].NumericScale); + length = TypeHelper.BlrAlign(length, 4); + length += 8; break; case IscCodes.SQL_BOOLEAN: blr.WriteByte(IscCodes.blr_bool); + length = TypeHelper.BlrAlign(length, 1); + length += 1; break; case IscCodes.SQL_TIMESTAMP_TZ_EX: blr.WriteByte(IscCodes.blr_ex_timestamp_tz); + length = TypeHelper.BlrAlign(length, 4); + length += 12; break; case IscCodes.SQL_TIMESTAMP_TZ: blr.WriteByte(IscCodes.blr_timestamp_tz); + length = TypeHelper.BlrAlign(length, 4); + length += 10; break; case IscCodes.SQL_TIME_TZ: blr.WriteByte(IscCodes.blr_sql_time_tz); + length = TypeHelper.BlrAlign(length, 4); + length += 6; break; case IscCodes.SQL_TIME_TZ_EX: blr.WriteByte(IscCodes.blr_ex_time_tz); + length = TypeHelper.BlrAlign(length, 4); + length += 8; break; case IscCodes.SQL_DEC16: blr.WriteByte(IscCodes.blr_dec64); + length = TypeHelper.BlrAlign(length, 8); + length += 8; break; case IscCodes.SQL_DEC34: blr.WriteByte(IscCodes.blr_dec128); + length = TypeHelper.BlrAlign(length, 8); + length += 16; break; case IscCodes.SQL_INT128: blr.WriteByte(IscCodes.blr_int128); blr.WriteByte((byte)_fields[i].NumericScale); + length = TypeHelper.BlrAlign(length, 8); + length += 16; break; case IscCodes.SQL_NULL: blr.WriteByte(IscCodes.blr_text); blr.WriteByte((byte)(len & 255)); blr.WriteByte((byte)(len >> 8)); + // no align + length += len; break; } blr.WriteByte(IscCodes.blr_short); blr.WriteByte(0); + + length = TypeHelper.BlrAlign(length, 2); + length += 2; } blr.WriteByte(IscCodes.blr_end); blr.WriteByte(IscCodes.blr_eoc); - return blr.ToArray(); + return new BlrData(blr.ToArray(), length); } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/EmptyDescriptorFiller.cs b/src/FirebirdSql.Data.FirebirdClient/Common/EmptyDescriptorFiller.cs new file mode 100644 index 00000000..5a7b41c1 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/EmptyDescriptorFiller.cs @@ -0,0 +1,38 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System.Threading; +using System.Threading.Tasks; + +namespace FirebirdSql.Data.Common +{ + internal sealed class EmptyDescriptorFiller : IDescriptorFiller + { + public static readonly EmptyDescriptorFiller Instance = new EmptyDescriptorFiller(); + + private EmptyDescriptorFiller() + { } + + public void Fill(Descriptor descriptor, int index) + { } + + public ValueTask FillAsync(Descriptor descriptor, int index, CancellationToken cancellationToken = default) + { + return ValueTask2.CompletedTask; + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs index 6e90a690..fad783b9 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs @@ -58,5 +58,9 @@ public static IEnumerable> Split(this T[] array, int size) yield return array.Skip(i * size).Take(size); } } + +#if NETSTANDARD2_0 + public static HashSet ToHashSet(this IEnumerable source) => new HashSet(source); +#endif } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/IDescriptorFiller.cs b/src/FirebirdSql.Data.FirebirdClient/Common/IDescriptorFiller.cs new file mode 100644 index 00000000..44b24ca1 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/IDescriptorFiller.cs @@ -0,0 +1,28 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System.Threading; +using System.Threading.Tasks; + +namespace FirebirdSql.Data.Common +{ + internal interface IDescriptorFiller + { + void Fill(Descriptor descriptor, int index); + ValueTask FillAsync(Descriptor descriptor, int index, CancellationToken cancellationToken = default); + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs b/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs index c64f9eff..afb64a94 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs @@ -223,6 +223,20 @@ internal static class IscCodes public const int op_crypt_key_callback = 97; public const int op_cond_accept = 98; + public const int op_batch_create = 99; + public const int op_batch_msg = 100; + public const int op_batch_exec = 101; + public const int op_batch_rls = 102; + public const int op_batch_cs = 103; + public const int op_batch_regblob = 104; + public const int op_batch_blob_stream = 105; + public const int op_batch_set_bpb = 106; + + public const int op_repl_data = 107; + public const int op_repl_req = 108; + + public const int op_batch_cancel = 109; + #endregion #region Database Parameter Block @@ -1044,5 +1058,23 @@ internal static class IscCodes public const int CNCT_client_crypt = 11; #endregion + + public static class Batch + { + public const int VERSION1 = 1; + + public const int TAG_MULTIERROR = 1; + public const int TAG_RECORD_COUNTS = 2; + public const int TAG_BUFFER_BYTES_SIZE = 3; + public const int TAG_BLOB_POLICY = 4; + public const int TAG_DETAILED_ERRORS = 5; + + public const int BLOB_NONE = 0; + public const int BLOB_ID_ENGINE = 1; + public const int BLOB_ID_USER = 2; + public const int BLOB_STREAM = 3; + + public const int BLOB_SEGHDR_ALIGN = 2; + } } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/NamedParametersParser.cs b/src/FirebirdSql.Data.FirebirdClient/Common/NamedParametersParser.cs new file mode 100644 index 00000000..174d0efd --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/NamedParametersParser.cs @@ -0,0 +1,89 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Collections.Generic; +using System.Text; + +namespace FirebirdSql.Data.Common +{ + internal static class NamedParametersParser + { + public static (string sql, IReadOnlyList parameters) Parse(string sql) + { + var sqlBuilder = new StringBuilder(sql.Length); + var paramBuilder = new StringBuilder(); + + if (sql.IndexOf('@') == -1) + { + return (sql, Array.Empty()); + } + + var namedParameters = new List(); + var inSingleQuotes = false; + var inDoubleQuotes = false; + var inParam = false; + for (var i = 0; i < sql.Length; i++) + { + var sym = sql[i]; + + if (inParam) + { + if (char.IsLetterOrDigit(sym) || sym == '_' || sym == '$') + { + paramBuilder.Append(sym); + } + else + { + namedParameters.Add(paramBuilder.ToString()); + paramBuilder.Length = 0; + sqlBuilder.Append('?'); + sqlBuilder.Append(sym); + inParam = false; + } + } + else + { + if (sym == '\'' && !inDoubleQuotes) + { + inSingleQuotes = !inSingleQuotes; + } + else if (sym == '\"' && !inSingleQuotes) + { + inDoubleQuotes = !inDoubleQuotes; + } + else if (!(inSingleQuotes || inDoubleQuotes) && sym == '@') + { + inParam = true; + paramBuilder.Append(sym); + continue; + } + + sqlBuilder.Append(sym); + } + } + + if (inParam) + { + namedParameters.Add(paramBuilder.ToString()); + sqlBuilder.Append('?'); + } + + return (sqlBuilder.ToString(), namedParameters); + } + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/ParameterBuffer.cs b/src/FirebirdSql.Data.FirebirdClient/Common/ParameterBuffer.cs index 1e4fe5a9..b2bf58da 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/ParameterBuffer.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/ParameterBuffer.cs @@ -23,6 +23,7 @@ namespace FirebirdSql.Data.Common { internal abstract class ParameterBuffer { +#warning Strictly speaking this should be disposed private MemoryStream _stream; public short Length => (short)_stream.Length; diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/StatementBase.cs b/src/FirebirdSql.Data.FirebirdClient/Common/StatementBase.cs index 7202a727..1cba97fc 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/StatementBase.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/StatementBase.cs @@ -224,17 +224,11 @@ public virtual async ValueTask ReleaseAsync(CancellationToken cancellationToken #region Abstract Methods - public abstract void Describe(); - public abstract ValueTask DescribeAsync(CancellationToken cancellationToken = default); - - public abstract void DescribeParameters(); - public abstract ValueTask DescribeParametersAsync(CancellationToken cancellationToken = default); - public abstract void Prepare(string commandText); public abstract ValueTask PrepareAsync(string commandText, CancellationToken cancellationToken = default); - public abstract void Execute(int timeout); - public abstract ValueTask ExecuteAsync(int timeout, CancellationToken cancellationToken = default); + public abstract void Execute(int timeout, IDescriptorFiller descriptorFiller); + public abstract ValueTask ExecuteAsync(int timeout, IDescriptorFiller descriptorFiller, CancellationToken cancellationToken = default); public abstract DbValue[] Fetch(); public abstract ValueTask FetchAsync(CancellationToken cancellationToken = default); @@ -251,6 +245,8 @@ public virtual async ValueTask ReleaseAsync(CancellationToken cancellationToken public abstract ArrayBase CreateArray(long handle, string tableName, string fieldName); public abstract ValueTask CreateArrayAsync(long handle, string tableName, string fieldName, CancellationToken cancellationToken = default); + public abstract BatchBase CreateBatch(); + #endregion #region Protected Abstract Methods diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/TypeHelper.cs b/src/FirebirdSql.Data.FirebirdClient/Common/TypeHelper.cs index 6b7d5049..ad5aaa66 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/TypeHelper.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/TypeHelper.cs @@ -817,5 +817,10 @@ public static Exception InvalidDataType(int type) { return new ArgumentException($"Invalid data type: {type}."); } + + public static int BlrAlign(int current, int alignment) + { + return (current + alignment - 1) & ~(alignment - 1); + } } } diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbBatchCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbBatchCommand.cs new file mode 100644 index 00000000..9dd0db10 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbBatchCommand.cs @@ -0,0 +1,1346 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Data.Common; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FirebirdSql.Data.Common; +using FirebirdSql.Data.Logging; + +namespace FirebirdSql.Data.FirebirdClient +{ +#warning Pull methods shared with FbCommand into some helper? + public sealed class FbBatchCommand : IFbPreparedCommand, IDescriptorFiller, IDisposable +#if !(NET48 || NETSTANDARD2_0) + , IAsyncDisposable +#endif + { + static readonly IFbLogger Log = FbLogManager.CreateLogger(nameof(FbBatchCommand)); + + #region Fields + + private FbConnection _connection; + private FbTransaction _transaction; +#warning Create FbBatchParameterCollection? + private List _batchParameters; + private StatementBase _statement; + //private FbDataReader _activeReader; + private IReadOnlyList _namedParameters; + private string _commandText; + private bool _disposed; + private bool _implicitTransaction; + //private int _commandTimeout; + //private int _fetchSize; + private bool _multiError; + + #endregion + + #region Properties + + public string CommandText + { + get { return _commandText; } + set + { + if (_commandText != value && _statement != null) + { + Release(); + } + + _commandText = value; + } + } + + //public int CommandTimeout + //{ + // get + // { + // if (_commandTimeout > 0) + // return _commandTimeout; + // if (_connection?.ConnectionOptions.CommandTimeout > 0) + // return (int)_connection?.ConnectionOptions.CommandTimeout; + // return 30; + // } + // set + // { + // if (value < 0) + // { + // throw new ArgumentException("The property value assigned is less than 0."); + // } + + // _commandTimeout = value; + // } + //} + + public FbConnection Connection + { + get { return _connection; } + set + { + //if (_activeReader != null) + //{ + // throw new InvalidOperationException("There is already an open DataReader associated with this Command which must be closed first."); + //} + + if (_transaction != null && _transaction.IsCompleted) + { + _transaction = null; + } + + if (_connection != null && + _connection != value && + _connection.State == ConnectionState.Open) + { + Release(); + } + + _connection = value; + } + } + + public List BatchParameters + { + get + { + if (_batchParameters == null) + { + _batchParameters = new List(); + } + return _batchParameters; + } + } + + public FbTransaction Transaction + { + get { return _implicitTransaction ? null : _transaction; } + set + { + //if (_activeReader != null) + //{ + // throw new InvalidOperationException("There is already an open DataReader associated with this Command which must be closed first."); + //} + + RollbackImplicitTransaction(); + + _transaction = value; + + if (_statement != null) + { + if (_transaction != null) + { + _statement.Transaction = _transaction.Transaction; + } + else + { + _statement.Transaction = null; + } + } + } + } + + //public int FetchSize + //{ + // get { return _fetchSize; } + // set + // { + // if (_activeReader != null) + // { + // throw new InvalidOperationException("There is already an open DataReader associated with this Command which must be closed first."); + // } + // _fetchSize = value; + // } + //} + + public bool MultiError + { + get { return _multiError; } + set { _multiError = value; } + } + + #endregion + + #region Internal Properties + + internal bool IsDisposed + { + get { return _disposed; } + } + + //internal FbDataReader ActiveReader + //{ + // get { return _activeReader; } + // set { _activeReader = value; } + //} + + internal FbTransaction ActiveTransaction + { + get { return _transaction; } + } + + internal bool HasImplicitTransaction + { + get { return _implicitTransaction; } + } + + //internal bool HasFields + //{ + // get { return _statement?.Fields?.Count > 0; } + //} + + internal bool HasParameters + { + get { return _batchParameters != null && _batchParameters.Count > 0 && _batchParameters[0].Count > 0; } + } + + #endregion + + #region Constructors + + public FbBatchCommand() + : this(null, null, null) + { } + + public FbBatchCommand(string cmdText) + : this(cmdText, null, null) + { } + + public FbBatchCommand(string cmdText, FbConnection connection) + : this(cmdText, connection, null) + { } + + public FbBatchCommand(string cmdText, FbConnection connection, FbTransaction transaction) + { + _namedParameters = Array.Empty(); + //_commandTimeout = 0; + //_fetchSize = 200; + _multiError = false; + _commandText = string.Empty; + + if (connection != null) + { + //_fetchSize = connection.ConnectionOptions.FetchSize; + } + + if (cmdText != null) + { + CommandText = cmdText; + } + + Connection = connection; + _transaction = transaction; + } + + #endregion + + #region IDisposable, IAsyncDisposable methods + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + try + { + Release(); + } + catch (IscException ex) + { + throw FbException.Create(ex); + } + _multiError = false; + //_commandTimeout = 0; + //_fetchSize = 0; + _implicitTransaction = false; + _commandText = null; + _connection = null; + _transaction = null; + _batchParameters = null; + _statement = null; + //_activeReader = null; + _namedParameters = null; + } + } +#if !(NET48 || NETSTANDARD2_0) + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + _disposed = true; + try + { + await ReleaseAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (IscException ex) + { + throw FbException.Create(ex); + } + _multiError = false; + //_commandTimeout = 0; + //_fetchSize = 0; + _implicitTransaction = false; + _commandText = null; + _connection = null; + _transaction = null; + _batchParameters = null; + _statement = null; + //_activeReader = null; + _namedParameters = null; + } + } +#endif + + #endregion + + #region Methods + + public void Cancel() + { + _connection.CancelCommand(); + } + + public FbParameter CreateParameter() + { + return new FbParameter(); + } + + public FbParameterCollection AddBatchParameters() + { + var result = new FbParameterCollection(); + BatchParameters.Add(result); + return result; + } + + public void Prepare() + { + CheckCommand(); + + using (var explicitCancellation = ExplicitCancellation.Enter(CancellationToken.None, Cancel)) + { + try + { + Prepare(false); + } + catch (IscException ex) + { + RollbackImplicitTransaction(); + throw FbException.Create(ex); + } + catch + { + RollbackImplicitTransaction(); + throw; + } + } + } + public async Task PrepareAsync(CancellationToken cancellationToken = default) + { + CheckCommand(); + + using (var explicitCancellation = ExplicitCancellation.Enter(cancellationToken, Cancel)) + { + try + { + await PrepareAsync(false, explicitCancellation.CancellationToken).ConfigureAwait(false); + } + catch (IscException ex) + { + await RollbackImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + throw FbException.Create(ex); + } + catch + { + await RollbackImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + throw; + } + } + } + + public FbBatchNonQueryResult ExecuteNonQuery() + { + CheckCommand(); + + FbBatchNonQueryResult result; + + using (var explicitCancellation = ExplicitCancellation.Enter(CancellationToken.None, Cancel)) + { + try + { + result = ExecuteCommand(false); + + //if (_statement.StatementType == DbStatementType.StoredProcedure) + //{ + // SetOutputParameters(); + //} + + CommitImplicitTransaction(); + } + catch (IscException ex) + { + RollbackImplicitTransaction(); + throw FbException.Create(ex); + } + catch + { + RollbackImplicitTransaction(); + throw; + } + } + + return result; + } + public async Task ExecuteNonQueryAsync(CancellationToken cancellationToken = default) + { + CheckCommand(); + + FbBatchNonQueryResult result; + + using (var explicitCancellation = ExplicitCancellation.Enter(cancellationToken, Cancel)) + { + try + { + result = await ExecuteCommandAsync(false, explicitCancellation.CancellationToken).ConfigureAwait(false); + + //if (_statement.StatementType == DbStatementType.StoredProcedure) + //{ + // await SetOutputParametersAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + //} + + await CommitImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + } + catch (IscException ex) + { + await RollbackImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + throw FbException.Create(ex); + } + catch + { + await RollbackImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + throw; + } + } + + return result; + } + + //public new FbDataReader ExecuteReader() => ExecuteReader(CommandBehavior.Default); + //public new Task ExecuteReaderAsync() => ExecuteReaderAsync(CommandBehavior.Default); + //public new Task ExecuteReaderAsync(CancellationToken cancellationToken) => ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); + + //public new FbDataReader ExecuteReader(CommandBehavior behavior) + //{ + // CheckCommand(); + + // using (var explicitCancellation = ExplicitCancellation.Enter(CancellationToken.None, Cancel)) + // { + // try + // { + // ExecuteCommand(behavior, true); + // } + // catch (IscException ex) + // { + // RollbackImplicitTransaction(); + // throw FbException.Create(ex); + // } + // catch + // { + // RollbackImplicitTransaction(); + // throw; + // } + // } + + // _activeReader = new FbDataReader(this, _connection, behavior); + // return _activeReader; + //} + //public new Task ExecuteReaderAsync(CommandBehavior behavior) => ExecuteReaderAsync(behavior, CancellationToken.None); + //public new async Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) + //{ + // CheckCommand(); + + // using (var explicitCancellation = ExplicitCancellation.Enter(cancellationToken, Cancel)) + // { + // try + // { + // await ExecuteCommandAsync(behavior, true, explicitCancellation.CancellationToken).ConfigureAwait(false); + // } + // catch (IscException ex) + // { + // await RollbackImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + // throw FbException.Create(ex); + // } + // catch + // { + // await RollbackImplicitTransactionAsync(explicitCancellation.CancellationToken).ConfigureAwait(false); + // throw; + // } + // } + + // _activeReader = new FbDataReader(this, _connection, behavior); + // return _activeReader; + //} + + public string GetCommandPlan() + { + if (_statement == null) + { + return null; + } + return _statement.GetExecutionPlan(); + } + public Task GetCommandPlanAsync(CancellationToken cancellationToken = default) + { + if (_statement == null) + { + return Task.FromResult(null); + } + return _statement.GetExecutionPlanAsync(cancellationToken).AsTask(); + } + + public string GetCommandExplainedPlan() + { + if (_statement == null) + { + return null; + } + return _statement.GetExecutionExplainedPlan(); + } + public Task GetCommandExplainedPlanAsync(CancellationToken cancellationToken = default) + { + if (_statement == null) + { + return Task.FromResult(null); + } + return _statement.GetExecutionExplainedPlanAsync(cancellationToken).AsTask(); + } + + #endregion + + #region Internal Methods + + // internal void DisposeReader() + // { + // if (_activeReader != null) + // { + //#if NET48 || NETSTANDARD2_0 + // _activeReader.Dispose(); + //#else + // _activeReader.Dispose(); + //#endif + // _activeReader = null; + // } + // } + // internal async Task DisposeReaderAsync(CancellationToken cancellationToken = default) + // { + // if (_activeReader != null) + // { + //#if NET48 || NETSTANDARD2_0 + // _activeReader.Dispose(); + // await Task.CompletedTask.ConfigureAwait(false); + //#else + // await _activeReader.DisposeAsync().ConfigureAwait(false); + //#endif + // _activeReader = null; + // } + // } + + //internal DbValue[] Fetch() + //{ + // if (_statement != null) + // { + // try + // { + // return _statement.Fetch(); + // } + // catch (IscException ex) + // { + // throw FbException.Create(ex); + // } + // } + // return null; + //} + //internal async Task FetchAsync(CancellationToken cancellationToken = default) + //{ + // if (_statement != null) + // { + // try + // { + // return await _statement.FetchAsync(cancellationToken).ConfigureAwait(false); + // } + // catch (IscException ex) + // { + // throw FbException.Create(ex); + // } + // } + // return null; + //} + + //internal Descriptor GetFieldsDescriptor() + //{ + // if (_statement != null) + // { + // return _statement.Fields; + // } + // return null; + //} + + //internal void SetOutputParameters() + //{ + // SetOutputParameters(null); + //} + //internal Task SetOutputParametersAsync(CancellationToken cancellationToken = default) + //{ + // return SetOutputParametersAsync(null, cancellationToken); + //} + + //internal void SetOutputParameters(DbValue[] outputParameterValues) + //{ + // if (Parameters.Count > 0 && _statement != null) + // { + // if (_statement != null && + // _statement.StatementType == DbStatementType.StoredProcedure) + // { + // var values = outputParameterValues; + // if (outputParameterValues == null) + // { + // values = _statement.GetOutputParameters(); + // } + + // if (values != null && values.Length > 0) + // { + // var i = 0; + // foreach (FbParameter parameter in Parameters) + // { + // if (parameter.Direction == ParameterDirection.Output || + // parameter.Direction == ParameterDirection.InputOutput || + // parameter.Direction == ParameterDirection.ReturnValue) + // { + // parameter.Value = values[i].GetValue(); + // i++; + // } + // } + // } + // } + // } + //} + //internal async Task SetOutputParametersAsync(DbValue[] outputParameterValues, CancellationToken cancellationToken = default) + //{ + // if (Parameters.Count > 0 && _statement != null) + // { + // if (_statement != null && + // _statement.StatementType == DbStatementType.StoredProcedure) + // { + // var values = outputParameterValues; + // if (outputParameterValues == null) + // { + // values = _statement.GetOutputParameters(); + // } + + // if (values != null && values.Length > 0) + // { + // var i = 0; + // foreach (FbParameter parameter in Parameters) + // { + // if (parameter.Direction == ParameterDirection.Output || + // parameter.Direction == ParameterDirection.InputOutput || + // parameter.Direction == ParameterDirection.ReturnValue) + // { + // parameter.Value = await values[i].GetValueAsync(cancellationToken).ConfigureAwait(false); + // i++; + // } + // } + // } + // } + // } + //} + + internal void CommitImplicitTransaction() + { + if (HasImplicitTransaction && _transaction != null && _transaction.Transaction != null) + { + try + { + _transaction.Commit(); + } + catch + { + RollbackImplicitTransaction(); + + throw; + } + finally + { + if (_transaction != null) + { +#if NET48 || NETSTANDARD2_0 + _transaction.Dispose(); +#else + _transaction.Dispose(); +#endif + _transaction = null; + _implicitTransaction = false; + } + + if (_statement != null) + { + _statement.Transaction = null; + } + } + } + } + internal async Task CommitImplicitTransactionAsync(CancellationToken cancellationToken = default) + { + if (HasImplicitTransaction && _transaction != null && _transaction.Transaction != null) + { + try + { + await _transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + await RollbackImplicitTransactionAsync(cancellationToken).ConfigureAwait(false); + + throw; + } + finally + { + if (_transaction != null) + { +#if NET48 || NETSTANDARD2_0 + _transaction.Dispose(); +#else + await _transaction.DisposeAsync().ConfigureAwait(false); +#endif + _transaction = null; + _implicitTransaction = false; + } + + if (_statement != null) + { + _statement.Transaction = null; + } + } + } + } + + internal void RollbackImplicitTransaction() + { + if (HasImplicitTransaction && _transaction != null && _transaction.Transaction != null) + { + var transactionCount = Connection.InnerConnection.Database.TransactionCount; + + try + { + _transaction.Rollback(); + } + catch + { + if (Connection.InnerConnection.Database.TransactionCount == transactionCount) + { + Connection.InnerConnection.Database.TransactionCount--; + } + } + finally + { +#if NET48 || NETSTANDARD2_0 + _transaction.Dispose(); +#else + _transaction.Dispose(); +#endif + _transaction = null; + _implicitTransaction = false; + + if (_statement != null) + { + _statement.Transaction = null; + } + } + } + } + internal async Task RollbackImplicitTransactionAsync(CancellationToken cancellationToken = default) + { + if (HasImplicitTransaction && _transaction != null && _transaction.Transaction != null) + { + var transactionCount = Connection.InnerConnection.Database.TransactionCount; + + try + { + await _transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + if (Connection.InnerConnection.Database.TransactionCount == transactionCount) + { + Connection.InnerConnection.Database.TransactionCount--; + } + } + finally + { +#if NET48 || NETSTANDARD2_0 + _transaction.Dispose(); +#else + await _transaction.DisposeAsync().ConfigureAwait(false); +#endif + _transaction = null; + _implicitTransaction = false; + + if (_statement != null) + { + _statement.Transaction = null; + } + } + } + } + + internal void Close() + { + if (_statement != null) + { + _statement.Close(); + } + } + internal Task CloseAsync(CancellationToken cancellationToken = default) + { + if (_statement != null) + { + return _statement.CloseAsync(cancellationToken).AsTask(); + } + return Task.CompletedTask; + } + + void IFbPreparedCommand.Release() => Release(); + internal void Release() + { + RollbackImplicitTransaction(); + + //DisposeReader(); + + if (_connection != null && _connection.State == ConnectionState.Open) + { + _connection.InnerConnection.RemovePreparedCommand(this); + } + + if (_statement != null) + { + _statement.Dispose2(); + _statement = null; + } + } + Task IFbPreparedCommand.ReleaseAsync(CancellationToken cancellationToken) => ReleaseAsync(cancellationToken); + internal async Task ReleaseAsync(CancellationToken cancellationToken = default) + { + await RollbackImplicitTransactionAsync(cancellationToken).ConfigureAwait(false); + + //await DisposeReaderAsync(cancellationToken).ConfigureAwait(false); + + if (_connection != null && _connection.State == ConnectionState.Open) + { + _connection.InnerConnection.RemovePreparedCommand(this); + } + + if (_statement != null) + { + await _statement.Dispose2Async(cancellationToken).ConfigureAwait(false); + _statement = null; + } + } + + void IFbPreparedCommand.TransactionCompleted() => TransactionCompleted(); + internal void TransactionCompleted() + { + if (Transaction != null) + { + //DisposeReader(); + Transaction = null; + } + } + Task IFbPreparedCommand.TransactionCompletedAsync(CancellationToken cancellationToken) => TransactionCompletedAsync(cancellationToken); + internal async Task TransactionCompletedAsync(CancellationToken cancellationToken = default) + { + if (Transaction != null) + { + //await DisposeReaderAsync(cancellationToken).ConfigureAwait(false); + await Task.CompletedTask.ConfigureAwait(false); + Transaction = null; + } + } + + #endregion + + #region IDescriptorFiller + + void IDescriptorFiller.Fill(Descriptor descriptor, int index) => UpdateParameterValues(descriptor, index); + private void UpdateParameterValues(Descriptor descriptor, int batchIndex) + { + if (!HasParameters) + return; + + for (var i = 0; i < descriptor.Count; i++) + { + var parameter = descriptor[i]; + var index = i; + + if (_namedParameters.Count > 0) + { + index = _batchParameters[batchIndex].IndexOf(_namedParameters[i], i); + if (index == -1) + { + throw FbException.Create($"Must declare the variable '{_namedParameters[i]}'."); + } + } + + if (index != -1) + { + var commandParameter = _batchParameters[batchIndex][index]; + if (commandParameter.InternalValue == DBNull.Value || commandParameter.InternalValue == null) + { + parameter.NullFlag = -1; + parameter.DbValue.SetValue(DBNull.Value); + + if (!parameter.AllowDBNull()) + { + parameter.DataType++; + } + } + else + { + parameter.NullFlag = 0; + + switch (parameter.DbDataType) + { + case DbDataType.Binary: + { + var blob = _statement.CreateBlob(); + blob.Write((byte[])commandParameter.InternalValue); + parameter.DbValue.SetValue(blob.Id); + } + break; + + case DbDataType.Text: + { + var blob = _statement.CreateBlob(); + if (commandParameter.InternalValue is byte[]) + { + blob.Write((byte[])commandParameter.InternalValue); + } + else + { + blob.Write((string)commandParameter.InternalValue); + } + parameter.DbValue.SetValue(blob.Id); + } + break; + + case DbDataType.Array: + { + if (parameter.ArrayHandle == null) + { + parameter.ArrayHandle = _statement.CreateArray(parameter.Relation, parameter.Name); + } + else + { + parameter.ArrayHandle.Database = _statement.Database; + parameter.ArrayHandle.Transaction = _statement.Transaction; + } + + parameter.ArrayHandle.Handle = 0; + parameter.ArrayHandle.Write((Array)commandParameter.InternalValue); + parameter.DbValue.SetValue(parameter.ArrayHandle.Handle); + } + break; + + case DbDataType.Guid: + if (!(commandParameter.InternalValue is Guid) && !(commandParameter.InternalValue is byte[])) + { + throw new InvalidOperationException("Incorrect Guid value."); + } + parameter.DbValue.SetValue(commandParameter.InternalValue); + break; + + default: + parameter.DbValue.SetValue(commandParameter.InternalValue); + break; + } + } + } + } + } + ValueTask IDescriptorFiller.FillAsync(Descriptor descriptor, int index, CancellationToken cancellationToken) => UpdateParameterValuesAsync(descriptor, index, cancellationToken); + private async ValueTask UpdateParameterValuesAsync(Descriptor descriptor, int batchIndex, CancellationToken cancellationToken = default) + { + if (!HasParameters) + return; + + for (var i = 0; i < descriptor.Count; i++) + { + var batchParameter = descriptor[i]; + var index = i; + + if (_namedParameters.Count > 0) + { + index = _batchParameters[batchIndex].IndexOf(_namedParameters[i], i); + if (index == -1) + { + throw FbException.Create($"Must declare the variable '{_namedParameters[i]}'."); + } + } + + if (index != -1) + { + var commandParameter = _batchParameters[batchIndex][index]; + if (commandParameter.InternalValue == DBNull.Value || commandParameter.InternalValue == null) + { + batchParameter.NullFlag = -1; + batchParameter.DbValue.SetValue(DBNull.Value); + + if (!batchParameter.AllowDBNull()) + { + batchParameter.DataType++; + } + } + else + { + batchParameter.NullFlag = 0; + + switch (batchParameter.DbDataType) + { + case DbDataType.Binary: + { + var blob = _statement.CreateBlob(); + await blob.WriteAsync((byte[])commandParameter.InternalValue, cancellationToken).ConfigureAwait(false); + batchParameter.DbValue.SetValue(blob.Id); + } + break; + + case DbDataType.Text: + { + var blob = _statement.CreateBlob(); + if (commandParameter.InternalValue is byte[]) + { + await blob.WriteAsync((byte[])commandParameter.InternalValue, cancellationToken).ConfigureAwait(false); + } + else + { + await blob.WriteAsync((string)commandParameter.InternalValue, cancellationToken).ConfigureAwait(false); + } + batchParameter.DbValue.SetValue(blob.Id); + } + break; + + case DbDataType.Array: + { + if (batchParameter.ArrayHandle == null) + { + batchParameter.ArrayHandle = + await _statement.CreateArrayAsync(batchParameter.Relation, batchParameter.Name, cancellationToken).ConfigureAwait(false); + } + else + { + batchParameter.ArrayHandle.Database = _statement.Database; + batchParameter.ArrayHandle.Transaction = _statement.Transaction; + } + + batchParameter.ArrayHandle.Handle = 0; + await batchParameter.ArrayHandle.WriteAsync((Array)commandParameter.InternalValue, cancellationToken).ConfigureAwait(false); + batchParameter.DbValue.SetValue(batchParameter.ArrayHandle.Handle); + } + break; + + case DbDataType.Guid: + if (!(commandParameter.InternalValue is Guid) && !(commandParameter.InternalValue is byte[])) + { + throw new InvalidOperationException("Incorrect Guid value."); + } + batchParameter.DbValue.SetValue(commandParameter.InternalValue); + break; + + default: + batchParameter.DbValue.SetValue(commandParameter.InternalValue); + break; + } + } + } + } + } + + #endregion + + #region Private Methods + + private void Prepare(bool returnsSet) + { + var innerConn = _connection.InnerConnection; + + // Check if we have a valid transaction + if (_transaction == null) + { + if (innerConn.IsEnlisted) + { + _transaction = innerConn.ActiveTransaction; + } + else + { + _implicitTransaction = true; + _transaction = new FbTransaction(_connection, _connection.ConnectionOptions.IsolationLevel); + _transaction.BeginTransaction(); + + // Update Statement transaction + if (_statement != null) + { + _statement.Transaction = _transaction.Transaction; + } + } + } + + // Check if we have a valid statement handle + if (_statement == null) + { + _statement = innerConn.Database.CreateStatement(_transaction.Transaction); + } + + // Prepare the statement if needed + if (!_statement.IsPrepared) + { + // Close the inner DataReader if needed + //DisposeReader(); + + // Reformat the SQL statement if needed + var sql = _commandText; + + try + { + (sql, _namedParameters) = NamedParametersParser.Parse(sql); + // Try to prepare the command + _statement.Prepare(sql); + } + catch + { + // Release the statement and rethrow the exception + _statement.Release(); + _statement = null; + + throw; + } + + // Add this command to the active command list + innerConn.AddPreparedCommand(this); + } + else + { + // Close statement for subsequently executions + Close(); + } + } + private async Task PrepareAsync(bool returnsSet, CancellationToken cancellationToken = default) + { + var innerConn = _connection.InnerConnection; + + // Check if we have a valid transaction + if (_transaction == null) + { + if (innerConn.IsEnlisted) + { + _transaction = innerConn.ActiveTransaction; + } + else + { + _implicitTransaction = true; + _transaction = new FbTransaction(_connection, _connection.ConnectionOptions.IsolationLevel); + await _transaction.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + + // Update Statement transaction + if (_statement != null) + { + _statement.Transaction = _transaction.Transaction; + } + } + } + + // Check if we have a valid statement handle + if (_statement == null) + { + _statement = innerConn.Database.CreateStatement(_transaction.Transaction); + } + + // Prepare the statement if needed + if (!_statement.IsPrepared) + { + // Close the inner DataReader if needed + //await DisposeReaderAsync(cancellationToken).ConfigureAwait(false); + + // Reformat the SQL statement if needed + var sql = _commandText; + + try + { + (sql, _namedParameters) = NamedParametersParser.Parse(sql); + // Try to prepare the command + await _statement.PrepareAsync(sql, cancellationToken).ConfigureAwait(false); + } + catch + { + // Release the statement and rethrow the exception + await _statement.ReleaseAsync(cancellationToken).ConfigureAwait(false); + _statement = null; + + throw; + } + + // Add this command to the active command list + innerConn.AddPreparedCommand(this); + } + else + { + // Close statement for subsequently executions + await CloseAsync(cancellationToken).ConfigureAwait(false); + } + } + + private FbBatchNonQueryResult ExecuteCommand(bool returnsSet) + { + LogCommandExecutionIfEnabled(); + + Prepare(returnsSet); + + // Set the fetch size + //_statement.FetchSize = _fetchSize; + + // Set if it's needed the Records Affected information + _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; + + // Validate input parameter count + if (_namedParameters.Count > 0 && !HasParameters) + { + throw FbException.Create("Must declare command parameters."); + } + + var batch = _statement.CreateBatch(); + try + { + batch.MultiError = MultiError; + // Execute + return new FbBatchNonQueryResult(batch.Execute(_batchParameters.Count, this)); + } + finally + { + batch.Dispose2(); + } + } + private async Task ExecuteCommandAsync(bool returnsSet, CancellationToken cancellationToken = default) + { + LogCommandExecutionIfEnabled(); + + await PrepareAsync(returnsSet, cancellationToken).ConfigureAwait(false); + + // Set the fetch size + //_statement.FetchSize = _fetchSize; + + // Set if it's needed the Records Affected information + _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; + + // Validate input parameter count + if (_namedParameters.Count > 0 && !HasParameters) + { + throw FbException.Create("Must declare command parameters."); + } + + var batch = _statement.CreateBatch(); + try + { + batch.MultiError = MultiError; + // Execute + return new FbBatchNonQueryResult(await batch.ExecuteAsync(_batchParameters.Count, this, cancellationToken).ConfigureAwait(false)); + } + finally + { + await batch.Dispose2Async(cancellationToken).ConfigureAwait(false); + } + } + + private void CheckCommand() + { + if (_transaction != null && _transaction.IsCompleted) + { + _transaction = null; + } + + FbConnection.EnsureOpen(_connection); + + //if (_activeReader != null) + //{ + // throw new InvalidOperationException("There is already an open DataReader associated with this Command which must be closed first."); + //} + + if (_transaction == null && + _connection.InnerConnection.HasActiveTransaction && + !_connection.InnerConnection.IsEnlisted) + { + throw new InvalidOperationException("Execute requires the Command object to have a Transaction object when the Connection object assigned to the command is in a pending local transaction. The Transaction property of the Command has not been initialized."); + } + + if (_transaction != null && !_transaction.IsCompleted && + !_connection.Equals(_transaction.Connection)) + { + throw new InvalidOperationException("Command Connection is not equal to Transaction Connection."); + } + + if (_commandText == null || _commandText.Length == 0) + { + throw new InvalidOperationException("The command text for this Command has not been set."); + } + } + + private void LogCommandExecutionIfEnabled() + { + if (Log.IsEnabled(FbLogLevel.Debug)) + { + var sb = new StringBuilder(); + sb.AppendLine("Executing command:"); + sb.AppendLine(_commandText); + if (FbLogManager.IsParameterLoggingEnabled) + { + sb.AppendLine("Parameters:"); + if (_batchParameters == null || _batchParameters.Count == 0 || _batchParameters[0].Count == 0) + { + sb.AppendLine(""); + } + else + { + foreach (var batchParameter in _batchParameters) + { + foreach (FbParameter parameter in batchParameter) + { + sb.AppendLine(string.Format("Name:{0}\tType:{1}\tUsed Value:{2}", parameter.ParameterName, parameter.FbDbType, (!IsNullParameterValue(parameter.InternalValue) ? parameter.InternalValue : ""))); + } + } + } + } + Log.Debug(sb.ToString()); + } + } + + private static bool IsNullParameterValue(object value) + { + return (value == DBNull.Value || value == null); + } + + #endregion + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbBatchNonQueryResult.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbBatchNonQueryResult.cs new file mode 100644 index 00000000..7e842735 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbBatchNonQueryResult.cs @@ -0,0 +1,56 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using static FirebirdSql.Data.Common.BatchBase; +using static FirebirdSql.Data.FirebirdClient.FbBatchNonQueryResult; + +namespace FirebirdSql.Data.FirebirdClient +{ + public sealed class FbBatchNonQueryResult : IEnumerable + { + public sealed class FbBatchNonQueryResultItem + { + public int RecordsAffected { get; internal set; } + public bool IsSuccess { get; internal set; } + public FbException Exception { get; internal set; } + } + + readonly List _items; + + public bool AllSuccess => _items.TrueForAll(x => x.IsSuccess); + public int Count => _items.Count; + + internal FbBatchNonQueryResult(ExecuteResultItem[] result) + { + _items = result.Select(x => new FbBatchNonQueryResultItem() + { + RecordsAffected = x.RecordsAffected, + IsSuccess = !x.IsError, + Exception = x.Exception != null ? (FbException)FbException.Create(x.Exception) : null, + }).ToList(); + } + + public FbBatchNonQueryResultItem this[int index] => _items[index]; + + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs index a9ce45db..4c5362a5 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs @@ -28,7 +28,7 @@ namespace FirebirdSql.Data.FirebirdClient { - public sealed class FbCommand : DbCommand, ICloneable + public sealed class FbCommand : DbCommand, IFbPreparedCommand, IDescriptorFiller, ICloneable { static readonly IFbLogger Log = FbLogManager.CreateLogger(nameof(FbCommand)); @@ -41,7 +41,7 @@ public sealed class FbCommand : DbCommand, ICloneable private FbParameterCollection _parameters; private StatementBase _statement; private FbDataReader _activeReader; - private List _namedParameters; + private IReadOnlyList _namedParameters; private string _commandText; private bool _disposed; private bool _designTimeVisible; @@ -275,6 +275,11 @@ internal bool HasFields get { return _statement?.Fields?.Count > 0; } } + internal bool HasParameters + { + get { return _parameters != null && _parameters.Count > 0; } + } + internal bool IsDDLCommand { get { return _statement?.StatementType == DbStatementType.DDL; } @@ -303,7 +308,7 @@ public FbCommand(string cmdText, FbConnection connection) public FbCommand(string cmdText, FbConnection connection, FbTransaction transaction) { - _namedParameters = new List(); + _namedParameters = Array.Empty(); _updatedRowSource = UpdateRowSource.Both; _commandType = CommandType.Text; _designTimeVisible = true; @@ -360,11 +365,7 @@ protected override void Dispose(bool disposing) _parameters = null; _statement = null; _activeReader = null; - if (_namedParameters != null) - { - _namedParameters.Clear(); - _namedParameters = null; - } + _namedParameters = null; } } base.Dispose(disposing); @@ -392,11 +393,7 @@ public override async ValueTask DisposeAsync() _parameters = null; _statement = null; _activeReader = null; - if (_namedParameters != null) - { - _namedParameters.Clear(); - _namedParameters = null; - } + _namedParameters = null; } await base.DisposeAsync().ConfigureAwait(false); } @@ -809,7 +806,6 @@ internal DbValue[] Fetch() { try { - // Fetch the next row return _statement.Fetch(); } catch (IscException ex) @@ -825,7 +821,6 @@ internal async Task FetchAsync(CancellationToken cancellationToken = { try { - // Fetch the next row return await _statement.FetchAsync(cancellationToken).ConfigureAwait(false); } catch (IscException ex) @@ -1069,6 +1064,7 @@ internal Task CloseAsync(CancellationToken cancellationToken = default) return Task.CompletedTask; } + void IFbPreparedCommand.Release() => Release(); internal void Release() { RollbackImplicitTransaction(); @@ -1086,6 +1082,7 @@ internal void Release() _statement = null; } } + Task IFbPreparedCommand.ReleaseAsync(CancellationToken cancellationToken) => ReleaseAsync(cancellationToken); internal async Task ReleaseAsync(CancellationToken cancellationToken = default) { await RollbackImplicitTransactionAsync(cancellationToken).ConfigureAwait(false); @@ -1104,220 +1101,43 @@ internal async Task ReleaseAsync(CancellationToken cancellationToken = default) } } - #endregion - - #region Input parameter descriptor generation methods - - private void DescribeInput() + void IFbPreparedCommand.TransactionCompleted() => TransactionCompleted(); + internal void TransactionCompleted() { - if (Parameters.Count > 0) + if (Transaction != null) { - var descriptor = BuildParametersDescriptor(); - if (descriptor == null) - { - _statement.DescribeParameters(); - } - else - { - _statement.Parameters = descriptor; - } - } - } - private async Task DescribeInputAsync(CancellationToken cancellationToken = default) - { - if (Parameters.Count > 0) - { - var descriptor = BuildParametersDescriptor(); - if (descriptor == null) - { - await _statement.DescribeParametersAsync(cancellationToken).ConfigureAwait(false); - } - else - { - _statement.Parameters = descriptor; - } - } - } - - private Descriptor BuildParametersDescriptor() - { - var count = ValidateInputParameters(); - - if (count > 0) - { - if (_namedParameters.Count > 0) - { - count = (short)_namedParameters.Count; - return BuildNamedParametersDescriptor(count); - } - else - { - return BuildPlaceHoldersDescriptor(count); - } - } - - return null; - } - - private Descriptor BuildNamedParametersDescriptor(short count) - { - var descriptor = new Descriptor(count); - var index = 0; - - for (var i = 0; i < _namedParameters.Count; i++) - { - var parametersIndex = Parameters.IndexOf(_namedParameters[i], i); - if (parametersIndex == -1) - { - throw FbException.Create($"Must declare the variable '{_namedParameters[i]}'."); - } - - var parameter = Parameters[parametersIndex]; - - if (parameter.Direction == ParameterDirection.Input || - parameter.Direction == ParameterDirection.InputOutput) - { - if (!BuildParameterDescriptor(descriptor, parameter, index++)) - { - return null; - } - } - } - - return descriptor; - } - - private Descriptor BuildPlaceHoldersDescriptor(short count) - { - var descriptor = new Descriptor(count); - var index = 0; - - for (var i = 0; i < Parameters.Count; i++) - { - var parameter = Parameters[i]; - - if (parameter.Direction == ParameterDirection.Input || - parameter.Direction == ParameterDirection.InputOutput) - { - if (!BuildParameterDescriptor(descriptor, parameter, index++)) - { - return null; - } - } + DisposeReader(); + Transaction = null; } - - return descriptor; } - - private bool BuildParameterDescriptor(Descriptor descriptor, FbParameter parameter, int index) + Task IFbPreparedCommand.TransactionCompletedAsync(CancellationToken cancellationToken) => TransactionCompletedAsync(cancellationToken); + internal async Task TransactionCompletedAsync(CancellationToken cancellationToken = default) { - if (!parameter.IsTypeSet) - { - return false; - } - - var type = parameter.FbDbType; - var charset = _connection.InnerConnection.Database.Charset; - - // Check the parameter character set - if (parameter.Charset == FbCharset.Octets && !(parameter.InternalValue is byte[])) - { - throw new InvalidOperationException("Value for char octets fields should be a byte array"); - } - else if (type == FbDbType.Guid) - { - charset = Charset.GetCharset(Charset.Octets); - } - else if (parameter.Charset != FbCharset.Default) + if (Transaction != null) { - charset = Charset.GetCharset((int)parameter.Charset); - } - - // Set parameter Data Type - descriptor[index].DataType = (short)TypeHelper.GetSqlTypeFromDbDataType(TypeHelper.GetDbDataTypeFromFbDbType(type), parameter.IsNullable); - - // Set parameter Sub Type - switch (type) - { - case FbDbType.Binary: - descriptor[index].SubType = 0; - break; - - case FbDbType.Text: - descriptor[index].SubType = 1; - break; - - case FbDbType.Guid: - descriptor[index].SubType = (short)charset.Identifier; - break; - - case FbDbType.Char: - case FbDbType.VarChar: - descriptor[index].SubType = (short)charset.Identifier; - if (charset.IsOctetsCharset) - { - descriptor[index].Length = (short)parameter.Size; - } - else if (parameter.HasSize) - { - var len = (short)(parameter.Size * charset.BytesPerCharacter); - descriptor[index].Length = len; - } - break; - } - - // Set parameter length - if (descriptor[index].Length == 0) - { - descriptor[index].Length = TypeHelper.GetSize((DbDataType)type) ?? 0; - } - - // Verify parameter - if (descriptor[index].SqlType == 0 || descriptor[index].Length == 0) - { - return false; + await DisposeReaderAsync(cancellationToken).ConfigureAwait(false); + Transaction = null; } - - return true; } - private short ValidateInputParameters() - { - short count = 0; - - for (var i = 0; i < Parameters.Count; i++) - { - if (Parameters[i].Direction == ParameterDirection.Input || - Parameters[i].Direction == ParameterDirection.InputOutput) - { - var type = Parameters[i].FbDbType; - - if (type == FbDbType.Array || type == FbDbType.Decimal || type == FbDbType.Numeric) - { - return -1; - } - else - { - count++; - } - } - } + #endregion - return count; - } + #region IDescriptorFiller - private void UpdateParameterValues() + void IDescriptorFiller.Fill(Descriptor descriptor, int index) => UpdateParameterValues(descriptor); + private void UpdateParameterValues(Descriptor descriptor) { - var index = -1; + if (!HasParameters) + return; - for (var i = 0; i < _statement.Parameters.Count; i++) + for (var i = 0; i < descriptor.Count; i++) { - var statementParameter = _statement.Parameters[i]; - index = i; + var parameter = descriptor[i]; + var index = i; if (_namedParameters.Count > 0) { - index = Parameters.IndexOf(_namedParameters[i], i); + index = _parameters.IndexOf(_namedParameters[i], i); if (index == -1) { throw FbException.Create($"Must declare the variable '{_namedParameters[i]}'."); @@ -1326,28 +1146,28 @@ private void UpdateParameterValues() if (index != -1) { - var commandParameter = Parameters[index]; + var commandParameter = _parameters[index]; if (commandParameter.InternalValue == DBNull.Value || commandParameter.InternalValue == null) { - statementParameter.NullFlag = -1; - statementParameter.DbValue.SetValue(DBNull.Value); + parameter.NullFlag = -1; + parameter.DbValue.SetValue(DBNull.Value); - if (!statementParameter.AllowDBNull()) + if (!parameter.AllowDBNull()) { - statementParameter.DataType++; + parameter.DataType++; } } else { - statementParameter.NullFlag = 0; + parameter.NullFlag = 0; - switch (statementParameter.DbDataType) + switch (parameter.DbDataType) { case DbDataType.Binary: { var blob = _statement.CreateBlob(); blob.Write((byte[])commandParameter.InternalValue); - statementParameter.DbValue.SetValue(blob.Id); + parameter.DbValue.SetValue(blob.Id); } break; @@ -1362,26 +1182,25 @@ private void UpdateParameterValues() { blob.Write((string)commandParameter.InternalValue); } - statementParameter.DbValue.SetValue(blob.Id); + parameter.DbValue.SetValue(blob.Id); } break; case DbDataType.Array: { - if (statementParameter.ArrayHandle == null) + if (parameter.ArrayHandle == null) { - statementParameter.ArrayHandle = - _statement.CreateArray(statementParameter.Relation, statementParameter.Name); + parameter.ArrayHandle = _statement.CreateArray(parameter.Relation, parameter.Name); } else { - statementParameter.ArrayHandle.Database = _statement.Database; - statementParameter.ArrayHandle.Transaction = _statement.Transaction; + parameter.ArrayHandle.Database = _statement.Database; + parameter.ArrayHandle.Transaction = _statement.Transaction; } - statementParameter.ArrayHandle.Handle = 0; - statementParameter.ArrayHandle.Write((Array)commandParameter.InternalValue); - statementParameter.DbValue.SetValue(statementParameter.ArrayHandle.Handle); + parameter.ArrayHandle.Handle = 0; + parameter.ArrayHandle.Write((Array)commandParameter.InternalValue); + parameter.DbValue.SetValue(parameter.ArrayHandle.Handle); } break; @@ -1390,29 +1209,31 @@ private void UpdateParameterValues() { throw new InvalidOperationException("Incorrect Guid value."); } - statementParameter.DbValue.SetValue(commandParameter.InternalValue); + parameter.DbValue.SetValue(commandParameter.InternalValue); break; default: - statementParameter.DbValue.SetValue(commandParameter.InternalValue); + parameter.DbValue.SetValue(commandParameter.InternalValue); break; } } } } } - private async Task UpdateParameterValuesAsync(CancellationToken cancellationToken = default) + ValueTask IDescriptorFiller.FillAsync(Descriptor descriptor, int index, CancellationToken cancellationToken) => UpdateParameterValuesAsync(descriptor, cancellationToken); + private async ValueTask UpdateParameterValuesAsync(Descriptor descriptor, CancellationToken cancellationToken = default) { - var index = -1; + if (!HasParameters) + return; - for (var i = 0; i < _statement.Parameters.Count; i++) + for (var i = 0; i < descriptor.Count; i++) { - var statementParameter = _statement.Parameters[i]; - index = i; + var statementParameter = descriptor[i]; + var index = i; if (_namedParameters.Count > 0) { - index = Parameters.IndexOf(_namedParameters[i], i); + index = _parameters.IndexOf(_namedParameters[i], i); if (index == -1) { throw FbException.Create($"Must declare the variable '{_namedParameters[i]}'."); @@ -1421,7 +1242,7 @@ private async Task UpdateParameterValuesAsync(CancellationToken cancellationToke if (index != -1) { - var commandParameter = Parameters[index]; + var commandParameter = _parameters[index]; if (commandParameter.InternalValue == DBNull.Value || commandParameter.InternalValue == null) { statementParameter.NullFlag = -1; @@ -1465,8 +1286,7 @@ private async Task UpdateParameterValuesAsync(CancellationToken cancellationToke { if (statementParameter.ArrayHandle == null) { - statementParameter.ArrayHandle = - await _statement.CreateArrayAsync(statementParameter.Relation, statementParameter.Name, cancellationToken).ConfigureAwait(false); + statementParameter.ArrayHandle = await _statement.CreateArrayAsync(statementParameter.Relation, statementParameter.Name, cancellationToken).ConfigureAwait(false); } else { @@ -1548,8 +1368,9 @@ private void Prepare(bool returnsSet) try { + (sql, _namedParameters) = NamedParametersParser.Parse(sql); // Try to prepare the command - _statement.Prepare(ParseNamedParameters(sql)); + _statement.Prepare(sql); } catch { @@ -1616,8 +1437,9 @@ private async Task PrepareAsync(bool returnsSet, CancellationToken cancellationT try { + (sql, _namedParameters) = NamedParametersParser.Parse(sql); // Try to prepare the command - await _statement.PrepareAsync(ParseNamedParameters(sql), cancellationToken).ConfigureAwait(false); + await _statement.PrepareAsync(sql, cancellationToken).ConfigureAwait(false); } catch { @@ -1657,23 +1479,13 @@ private void ExecuteCommand(CommandBehavior behavior, bool returnsSet) _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; // Validate input parameter count - if (_namedParameters.Count > 0 && Parameters.Count == 0) + if (_namedParameters.Count > 0 && !HasParameters) { throw FbException.Create("Must declare command parameters."); } - // Update input parameter values - if (Parameters.Count > 0) - { - if (_statement.Parameters == null) - { - DescribeInput(); - } - UpdateParameterValues(); - } - - // Execute statement - _statement.Execute(CommandTimeout * 1000); + // Execute + _statement.Execute(CommandTimeout * 1000, this); } } private async Task ExecuteCommandAsync(CommandBehavior behavior, bool returnsSet, CancellationToken cancellationToken = default) @@ -1695,23 +1507,13 @@ private async Task ExecuteCommandAsync(CommandBehavior behavior, bool returnsSet _statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected; // Validate input parameter count - if (_namedParameters.Count > 0 && Parameters.Count == 0) + if (_namedParameters.Count > 0 && !HasParameters) { throw FbException.Create("Must declare command parameters."); } - // Update input parameter values - if (Parameters.Count > 0) - { - if (_statement.Parameters == null) - { - await DescribeInputAsync(cancellationToken).ConfigureAwait(false); - } - await UpdateParameterValuesAsync(cancellationToken).ConfigureAwait(false); - } - - // Execute statement - await _statement.ExecuteAsync(CommandTimeout * 1000, cancellationToken).ConfigureAwait(false); + // Execute + await _statement.ExecuteAsync(CommandTimeout * 1000, this, cancellationToken).ConfigureAwait(false); } } @@ -1761,70 +1563,6 @@ private string BuildStoredProcedureSql(string spName, bool returnsSet) return sql; } - private string ParseNamedParameters(string sql) - { - var builder = new StringBuilder(); - var paramBuilder = new StringBuilder(); - var inSingleQuotes = false; - var inDoubleQuotes = false; - var inParam = false; - - _namedParameters.Clear(); - - if (sql.IndexOf('@') == -1) - { - return sql; - } - - for (var i = 0; i < sql.Length; i++) - { - var sym = sql[i]; - - if (inParam) - { - if (char.IsLetterOrDigit(sym) || sym == '_' || sym == '$') - { - paramBuilder.Append(sym); - } - else - { - _namedParameters.Add(paramBuilder.ToString()); - paramBuilder.Length = 0; - builder.Append('?'); - builder.Append(sym); - inParam = false; - } - } - else - { - if (sym == '\'' && !inDoubleQuotes) - { - inSingleQuotes = !inSingleQuotes; - } - else if (sym == '\"' && !inSingleQuotes) - { - inDoubleQuotes = !inDoubleQuotes; - } - else if (!(inSingleQuotes || inDoubleQuotes) && sym == '@') - { - inParam = true; - paramBuilder.Append(sym); - continue; - } - - builder.Append(sym); - } - } - - if (inParam) - { - _namedParameters.Add(paramBuilder.ToString()); - builder.Append('?'); - } - - return builder.ToString(); - } - private void CheckCommand() { if (_transaction != null && _transaction.IsCompleted) @@ -1868,16 +1606,16 @@ private void LogCommandExecutionIfEnabled() if (FbLogManager.IsParameterLoggingEnabled) { sb.AppendLine("Parameters:"); - if (_parameters?.Count > 0) + if (!HasParameters) { - foreach (FbParameter item in _parameters) - { - sb.AppendLine(string.Format("Name:{0}\tType:{1}\tUsed Value:{2}", item.ParameterName, item.FbDbType, (!IsNullParameterValue(item.InternalValue) ? item.InternalValue : ""))); - } + sb.AppendLine(""); } else { - sb.AppendLine(""); + foreach (FbParameter parameter in _parameters) + { + sb.AppendLine(string.Format("Name:{0}\tType:{1}\tUsed Value:{2}", parameter.ParameterName, parameter.FbDbType, (!IsNullParameterValue(parameter.InternalValue) ? parameter.InternalValue : ""))); + } } } Log.Debug(sb.ToString()); diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs index 14bd58ba..49776e67 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnection.cs @@ -469,6 +469,23 @@ protected override DbCommand CreateDbCommand() return new FbCommand(null, this); } +#if NET6_0_OR_GREATER + public new DbBatch CreateBatch() + { + throw new NotSupportedException("DbBatch is currently not supported. Use FbBatchCommand instead."); + } + + protected override DbBatch CreateDbBatch() + { + return CreateBatch(); + } +#endif + + public FbBatchCommand CreateBatchCommand() + { + return new FbBatchCommand(null, this); + } + public override void ChangeDatabase(string db) { CheckClosed(); @@ -824,9 +841,9 @@ async Task DisconnectEnlistedHelper() } } - #endregion +#endregion - #region Private Methods +#region Private Methods private void CheckClosed() { @@ -836,9 +853,9 @@ private void CheckClosed() } } - #endregion +#endregion - #region Event Handlers +#region Event Handlers private void OnWarningMessage(IscException warning) { @@ -851,9 +868,9 @@ private void OnStateChange(ConnectionState originalState, ConnectionState curren StateChange?.Invoke(this, new StateChangeEventArgs(originalState, currentState)); } - #endregion +#endregion - #region Cancelation +#region Cancelation public void EnableCancel() { CheckClosed(); @@ -892,9 +909,9 @@ public Task CancelCommandAsync(CancellationToken cancellationToken = default) return _innerConnection.CancelCommandAsync(cancellationToken); } - #endregion +#endregion - #region Internal Methods +#region Internal Methods internal static void EnsureOpen(FbConnection connection) { @@ -902,6 +919,6 @@ internal static void EnsureOpen(FbConnection connection) throw new InvalidOperationException("Connection must be valid and open."); } - #endregion +#endregion } } diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionInternal.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionInternal.cs index 48251d45..14d6b21c 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionInternal.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbConnectionInternal.cs @@ -36,7 +36,7 @@ internal class FbConnectionInternal private DatabaseBase _db; private FbTransaction _activeTransaction; - private HashSet _preparedCommands; + private HashSet _preparedCommands; private ConnectionString _options; private FbConnection _owningConnection; private FbEnlistmentNotification _enlistmentNotification; @@ -89,7 +89,7 @@ public ConnectionString Options public FbConnectionInternal(ConnectionString options) { - _preparedCommands = new HashSet(); + _preparedCommands = new HashSet(); _options = options; } @@ -456,22 +456,14 @@ public void TransactionCompleted() { foreach (var command in _preparedCommands) { - if (command.Transaction != null) - { - command.DisposeReader(); - command.Transaction = null; - } + command.TransactionCompleted(); } } public async Task TransactionCompletedAsync(CancellationToken cancellationToken = default) { foreach (var command in _preparedCommands) { - if (command.Transaction != null) - { - await command.DisposeReaderAsync(cancellationToken).ConfigureAwait(false); - command.Transaction = null; - } + await command.TransactionCompletedAsync(cancellationToken).ConfigureAwait(false); } } @@ -551,14 +543,14 @@ public Task GetSchemaAsync(string collectionName, string[] restrictio #region Prepared Commands Methods - public void AddPreparedCommand(FbCommand command) + public void AddPreparedCommand(IFbPreparedCommand command) { if (_preparedCommands.Contains(command)) return; _preparedCommands.Add(command); } - public void RemovePreparedCommand(FbCommand command) + public void RemovePreparedCommand(IFbPreparedCommand command) { _preparedCommands.Remove(command); } diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/IFbPreparedCommand.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/IFbPreparedCommand.cs new file mode 100644 index 00000000..f22d6ae6 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/IFbPreparedCommand.cs @@ -0,0 +1,30 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Jiri Cincura (jiri@cincura.net) + +using System.Threading; +using System.Threading.Tasks; + +namespace FirebirdSql.Data.FirebirdClient +{ + internal interface IFbPreparedCommand + { + void Release(); + Task ReleaseAsync(CancellationToken cancellationToken = default); + void TransactionCompleted(); + Task TransactionCompletedAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj index 762fe6cf..4d191e0e 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj @@ -57,5 +57,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + +