From c5b9fb9ea7cc7f5f70dcfff58b8685ae0eac3d9a Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Oct 2024 00:43:01 -0700 Subject: [PATCH 01/11] Fix FbZonedDateTime invalid cast (#1194) --- .../FbZonedDateTimeTypeTests.cs | 8 ++++ .../Types/FbZonedDateTime.cs | 41 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedDateTimeTypeTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedDateTimeTypeTests.cs index ca94f33e..b69dc704 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedDateTimeTypeTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedDateTimeTypeTests.cs @@ -47,6 +47,14 @@ public void EqualityFalse(FbZonedDateTime expected, FbZonedDateTime actual) Assert.AreNotEqual(expected, actual); } + [Test] + public void ConvertToDateTimeShouldNotThrow() + { + var fbZonedDateTime = new FbZonedDateTime(new DateTime(2020, 12, 4, 10, 38, 0, DateTimeKind.Utc), "UTC"); + + Assert.DoesNotThrow(() => Convert.ChangeType(fbZonedDateTime, typeof(DateTime))); + } + public void DateTimeShouldBeUtc() { Assert.Throws(() => diff --git a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs index b2eff99b..78ad2c5a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs @@ -21,7 +21,7 @@ namespace FirebirdSql.Data.Types; [StructLayout(LayoutKind.Auto)] -public readonly struct FbZonedDateTime : IEquatable +public readonly struct FbZonedDateTime : IEquatable, IConvertible { public DateTime DateTime { get; } public string TimeZone { get; } @@ -72,8 +72,43 @@ public override int GetHashCode() } } - public bool Equals(FbZonedDateTime other) => DateTime.Equals(other.DateTime) && TimeZone.Equals(other.TimeZone, StringComparison.OrdinalIgnoreCase); - + public bool Equals(FbZonedDateTime other) => DateTime.Equals(other.DateTime) && TimeZone.Equals(other.TimeZone, StringComparison.OrdinalIgnoreCase); + + TypeCode IConvertible.GetTypeCode() => TypeCode.Object; + + DateTime IConvertible.ToDateTime(IFormatProvider provider) => DateTime; + + string IConvertible.ToString(IFormatProvider provider) => ToString(); + + object IConvertible.ToType(Type conversionType, IFormatProvider provider) + => ReferenceEquals(conversionType, typeof(FbZonedDateTime)) ? this : throw new InvalidCastException(conversionType?.FullName); + + bool IConvertible.ToBoolean(IFormatProvider provider) => throw new InvalidCastException(nameof(Boolean)); + + byte IConvertible.ToByte(IFormatProvider provider) => throw new InvalidCastException(nameof(Byte)); + + char IConvertible.ToChar(IFormatProvider provider) => throw new InvalidCastException(nameof(Char)); + + decimal IConvertible.ToDecimal(IFormatProvider provider) => throw new InvalidCastException(nameof(Decimal)); + + double IConvertible.ToDouble(IFormatProvider provider) => throw new InvalidCastException(nameof(Double)); + + short IConvertible.ToInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(Int16)); + + int IConvertible.ToInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(Int32)); + + long IConvertible.ToInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(Int64)); + + sbyte IConvertible.ToSByte(IFormatProvider provider) => throw new InvalidCastException(nameof(SByte)); + + float IConvertible.ToSingle(IFormatProvider provider) => throw new InvalidCastException(nameof(Single)); + + ushort IConvertible.ToUInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt16)); + + uint IConvertible.ToUInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt32)); + + ulong IConvertible.ToUInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt64)); + public static bool operator ==(FbZonedDateTime lhs, FbZonedDateTime rhs) => lhs.Equals(rhs); public static bool operator !=(FbZonedDateTime lhs, FbZonedDateTime rhs) => lhs.Equals(rhs); From cb6293e563ad4857dc2bd2616619b1c3bd1fea09 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 17 Oct 2024 00:45:18 -0700 Subject: [PATCH 02/11] Implement IConvertible for FbZonedTime (#1195) --- .../FbZonedTimeTypeTests.cs | 8 ++++ .../Types/FbZonedTime.cs | 45 +++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedTimeTypeTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedTimeTypeTests.cs index c0309b38..959c2a57 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedTimeTypeTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbZonedTimeTypeTests.cs @@ -46,4 +46,12 @@ public void EqualityFalse(FbZonedTime expected, FbZonedTime actual) { Assert.AreNotEqual(expected, actual); } + + [Test] + public void ConvertToTimeSpanShouldNotThrow() + { + var fbZonedTime = new FbZonedTime(TimeSpan.FromMinutes(142), "UTC"); + + Assert.DoesNotThrow(() => Convert.ChangeType(fbZonedTime, typeof(TimeSpan))); + } } diff --git a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs index 56003854..47636893 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs @@ -21,7 +21,7 @@ namespace FirebirdSql.Data.Types; [StructLayout(LayoutKind.Auto)] -public readonly struct FbZonedTime : IEquatable +public readonly struct FbZonedTime : IEquatable, IConvertible { public TimeSpan Time { get; } public string TimeZone { get; } @@ -70,8 +70,47 @@ public override int GetHashCode() } } - public bool Equals(FbZonedTime other) => Time.Equals(other.Time) && TimeZone.Equals(other.TimeZone, StringComparison.OrdinalIgnoreCase); - + public bool Equals(FbZonedTime other) => Time.Equals(other.Time) && TimeZone.Equals(other.TimeZone, StringComparison.OrdinalIgnoreCase); + + TypeCode IConvertible.GetTypeCode() => TypeCode.Object; + + string IConvertible.ToString(IFormatProvider provider) => ToString(); + + object IConvertible.ToType(Type conversionType, IFormatProvider provider) + => ReferenceEquals(conversionType, typeof(FbZonedTime)) + ? this + : ReferenceEquals(conversionType, typeof(TimeSpan)) + ? Time + : throw new InvalidCastException(conversionType?.FullName); + + bool IConvertible.ToBoolean(IFormatProvider provider) => throw new InvalidCastException(nameof(Boolean)); + + byte IConvertible.ToByte(IFormatProvider provider) => throw new InvalidCastException(nameof(Byte)); + + char IConvertible.ToChar(IFormatProvider provider) => throw new InvalidCastException(nameof(Char)); + + DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw new InvalidCastException(nameof(DateTime)); + + decimal IConvertible.ToDecimal(IFormatProvider provider) => throw new InvalidCastException(nameof(Decimal)); + + double IConvertible.ToDouble(IFormatProvider provider) => throw new InvalidCastException(nameof(Double)); + + short IConvertible.ToInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(Int16)); + + int IConvertible.ToInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(Int32)); + + long IConvertible.ToInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(Int64)); + + sbyte IConvertible.ToSByte(IFormatProvider provider) => throw new InvalidCastException(nameof(SByte)); + + float IConvertible.ToSingle(IFormatProvider provider) => throw new InvalidCastException(nameof(Single)); + + ushort IConvertible.ToUInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt16)); + + uint IConvertible.ToUInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt32)); + + ulong IConvertible.ToUInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt64)); + public static bool operator ==(FbZonedTime lhs, FbZonedTime rhs) => lhs.Equals(rhs); public static bool operator !=(FbZonedTime lhs, FbZonedTime rhs) => lhs.Equals(rhs); From f00a9a9aba30c7622a1d375c2c020ca3c6fe1954 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Thu, 17 Oct 2024 09:47:47 +0200 Subject: [PATCH 03/11] Small cleanup after PRs. --- .../Types/FbZonedDateTime.cs | 172 +++++++++--------- .../Types/FbZonedTime.cs | 165 ++++++++--------- 2 files changed, 157 insertions(+), 180 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs index 78ad2c5a..54e7e5c7 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedDateTime.cs @@ -1,77 +1,77 @@ -/* - * 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.Runtime.InteropServices; - -namespace FirebirdSql.Data.Types; - -[StructLayout(LayoutKind.Auto)] -public readonly struct FbZonedDateTime : IEquatable, IConvertible -{ - public DateTime DateTime { get; } - public string TimeZone { get; } - public TimeSpan? Offset { get; } - - internal FbZonedDateTime(DateTime dateTime, string timeZone, TimeSpan? offset) - { - if (dateTime.Kind != DateTimeKind.Utc) - throw new ArgumentException("Value must be in UTC.", nameof(dateTime)); - if (timeZone == null) - throw new ArgumentNullException(nameof(timeZone)); - if (string.IsNullOrWhiteSpace(timeZone)) - throw new ArgumentException(nameof(timeZone)); - - DateTime = dateTime; - TimeZone = timeZone; - Offset = offset; - } - - public FbZonedDateTime(DateTime dateTime, string timeZone) - : this(dateTime, timeZone, null) - { } - - public override string ToString() - { - if (Offset != null) - { - return $"{DateTime} {TimeZone} ({Offset})"; - } - return $"{DateTime} {TimeZone}"; - } - - public override bool Equals(object obj) - { - return obj is FbZonedDateTime fbZonedDateTime && Equals(fbZonedDateTime); - } - - public override int GetHashCode() - { - unchecked - { - var hash = (int)2166136261; - hash = (hash * 16777619) ^ DateTime.GetHashCode(); - hash = (hash * 16777619) ^ TimeZone.GetHashCode(); - if (Offset != null) - hash = (hash * 16777619) ^ Offset.GetHashCode(); - return hash; - } - } - +/* + * 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.Runtime.InteropServices; + +namespace FirebirdSql.Data.Types; + +[StructLayout(LayoutKind.Auto)] +public readonly struct FbZonedDateTime : IEquatable, IConvertible +{ + public DateTime DateTime { get; } + public string TimeZone { get; } + public TimeSpan? Offset { get; } + + internal FbZonedDateTime(DateTime dateTime, string timeZone, TimeSpan? offset) + { + if (dateTime.Kind != DateTimeKind.Utc) + throw new ArgumentException("Value must be in UTC.", nameof(dateTime)); + if (timeZone == null) + throw new ArgumentNullException(nameof(timeZone)); + if (string.IsNullOrWhiteSpace(timeZone)) + throw new ArgumentException(nameof(timeZone)); + + DateTime = dateTime; + TimeZone = timeZone; + Offset = offset; + } + + public FbZonedDateTime(DateTime dateTime, string timeZone) + : this(dateTime, timeZone, null) + { } + + public override string ToString() + { + if (Offset != null) + { + return $"{DateTime} {TimeZone} ({Offset})"; + } + return $"{DateTime} {TimeZone}"; + } + + public override bool Equals(object obj) + { + return obj is FbZonedDateTime fbZonedDateTime && Equals(fbZonedDateTime); + } + + public override int GetHashCode() + { + unchecked + { + var hash = (int)2166136261; + hash = (hash * 16777619) ^ DateTime.GetHashCode(); + hash = (hash * 16777619) ^ TimeZone.GetHashCode(); + if (Offset != null) + hash = (hash * 16777619) ^ Offset.GetHashCode(); + return hash; + } + } + public bool Equals(FbZonedDateTime other) => DateTime.Equals(other.DateTime) && TimeZone.Equals(other.TimeZone, StringComparison.OrdinalIgnoreCase); TypeCode IConvertible.GetTypeCode() => TypeCode.Object; @@ -81,35 +81,25 @@ public override int GetHashCode() string IConvertible.ToString(IFormatProvider provider) => ToString(); object IConvertible.ToType(Type conversionType, IFormatProvider provider) - => ReferenceEquals(conversionType, typeof(FbZonedDateTime)) ? this : throw new InvalidCastException(conversionType?.FullName); + => ReferenceEquals(conversionType, typeof(FbZonedDateTime)) + ? this + : throw new InvalidCastException(conversionType?.FullName); bool IConvertible.ToBoolean(IFormatProvider provider) => throw new InvalidCastException(nameof(Boolean)); - byte IConvertible.ToByte(IFormatProvider provider) => throw new InvalidCastException(nameof(Byte)); - char IConvertible.ToChar(IFormatProvider provider) => throw new InvalidCastException(nameof(Char)); - decimal IConvertible.ToDecimal(IFormatProvider provider) => throw new InvalidCastException(nameof(Decimal)); - double IConvertible.ToDouble(IFormatProvider provider) => throw new InvalidCastException(nameof(Double)); - short IConvertible.ToInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(Int16)); - int IConvertible.ToInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(Int32)); - long IConvertible.ToInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(Int64)); - sbyte IConvertible.ToSByte(IFormatProvider provider) => throw new InvalidCastException(nameof(SByte)); - float IConvertible.ToSingle(IFormatProvider provider) => throw new InvalidCastException(nameof(Single)); - ushort IConvertible.ToUInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt16)); - uint IConvertible.ToUInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt32)); - ulong IConvertible.ToUInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt64)); - public static bool operator ==(FbZonedDateTime lhs, FbZonedDateTime rhs) => lhs.Equals(rhs); - - public static bool operator !=(FbZonedDateTime lhs, FbZonedDateTime rhs) => lhs.Equals(rhs); -} + public static bool operator ==(FbZonedDateTime lhs, FbZonedDateTime rhs) => lhs.Equals(rhs); + + public static bool operator !=(FbZonedDateTime lhs, FbZonedDateTime rhs) => lhs.Equals(rhs); +} diff --git a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs index 47636893..f4f34506 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Types/FbZonedTime.cs @@ -1,75 +1,75 @@ -/* - * 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.Runtime.InteropServices; - -namespace FirebirdSql.Data.Types; - -[StructLayout(LayoutKind.Auto)] -public readonly struct FbZonedTime : IEquatable, IConvertible -{ - public TimeSpan Time { get; } - public string TimeZone { get; } - public TimeSpan? Offset { get; } - - internal FbZonedTime(TimeSpan time, string timeZone, TimeSpan? offset) - { - if (timeZone == null) - throw new ArgumentNullException(nameof(timeZone)); - if (string.IsNullOrWhiteSpace(timeZone)) - throw new ArgumentException(nameof(timeZone)); - - Time = time; - TimeZone = timeZone; - Offset = offset; - } - - public FbZonedTime(TimeSpan time, string timeZone) - : this(time, timeZone, null) - { } - - public override string ToString() - { - if (Offset != null) - { - return $"{Time} {TimeZone} ({Offset})"; - } - return $"{Time} {TimeZone}"; - } - - public override bool Equals(object obj) - { - return obj is FbZonedTime fbZonedTime && Equals(fbZonedTime); - } - - public override int GetHashCode() - { - unchecked - { - var hash = (int)2166136261; - hash = (hash * 16777619) ^ Time.GetHashCode(); - hash = (hash * 16777619) ^ TimeZone.GetHashCode(); - if (Offset != null) - hash = (hash * 16777619) ^ Offset.GetHashCode(); - return hash; - } - } - +/* + * 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.Runtime.InteropServices; + +namespace FirebirdSql.Data.Types; + +[StructLayout(LayoutKind.Auto)] +public readonly struct FbZonedTime : IEquatable, IConvertible +{ + public TimeSpan Time { get; } + public string TimeZone { get; } + public TimeSpan? Offset { get; } + + internal FbZonedTime(TimeSpan time, string timeZone, TimeSpan? offset) + { + if (timeZone == null) + throw new ArgumentNullException(nameof(timeZone)); + if (string.IsNullOrWhiteSpace(timeZone)) + throw new ArgumentException(nameof(timeZone)); + + Time = time; + TimeZone = timeZone; + Offset = offset; + } + + public FbZonedTime(TimeSpan time, string timeZone) + : this(time, timeZone, null) + { } + + public override string ToString() + { + if (Offset != null) + { + return $"{Time} {TimeZone} ({Offset})"; + } + return $"{Time} {TimeZone}"; + } + + public override bool Equals(object obj) + { + return obj is FbZonedTime fbZonedTime && Equals(fbZonedTime); + } + + public override int GetHashCode() + { + unchecked + { + var hash = (int)2166136261; + hash = (hash * 16777619) ^ Time.GetHashCode(); + hash = (hash * 16777619) ^ TimeZone.GetHashCode(); + if (Offset != null) + hash = (hash * 16777619) ^ Offset.GetHashCode(); + return hash; + } + } + public bool Equals(FbZonedTime other) => Time.Equals(other.Time) && TimeZone.Equals(other.TimeZone, StringComparison.OrdinalIgnoreCase); TypeCode IConvertible.GetTypeCode() => TypeCode.Object; @@ -84,34 +84,21 @@ object IConvertible.ToType(Type conversionType, IFormatProvider provider) : throw new InvalidCastException(conversionType?.FullName); bool IConvertible.ToBoolean(IFormatProvider provider) => throw new InvalidCastException(nameof(Boolean)); - byte IConvertible.ToByte(IFormatProvider provider) => throw new InvalidCastException(nameof(Byte)); - char IConvertible.ToChar(IFormatProvider provider) => throw new InvalidCastException(nameof(Char)); - DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw new InvalidCastException(nameof(DateTime)); - decimal IConvertible.ToDecimal(IFormatProvider provider) => throw new InvalidCastException(nameof(Decimal)); - double IConvertible.ToDouble(IFormatProvider provider) => throw new InvalidCastException(nameof(Double)); - short IConvertible.ToInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(Int16)); - int IConvertible.ToInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(Int32)); - long IConvertible.ToInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(Int64)); - sbyte IConvertible.ToSByte(IFormatProvider provider) => throw new InvalidCastException(nameof(SByte)); - float IConvertible.ToSingle(IFormatProvider provider) => throw new InvalidCastException(nameof(Single)); - ushort IConvertible.ToUInt16(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt16)); - uint IConvertible.ToUInt32(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt32)); - ulong IConvertible.ToUInt64(IFormatProvider provider) => throw new InvalidCastException(nameof(UInt64)); - public static bool operator ==(FbZonedTime lhs, FbZonedTime rhs) => lhs.Equals(rhs); - - public static bool operator !=(FbZonedTime lhs, FbZonedTime rhs) => lhs.Equals(rhs); -} + public static bool operator ==(FbZonedTime lhs, FbZonedTime rhs) => lhs.Equals(rhs); + + public static bool operator !=(FbZonedTime lhs, FbZonedTime rhs) => lhs.Equals(rhs); +} From 128156f083aeaa82b1516d45ba86aec910a2ae6c Mon Sep 17 00:00:00 2001 From: Lars Weber Date: Tue, 12 Nov 2024 12:40:59 +0000 Subject: [PATCH 04/11] FbBlobStream (#127) --- .../BlobStreamTests.cs | 86 +++++++ .../FbBlobTests.cs | 1 + .../Client/Managed/Version10/GdsBlob.cs | 233 ++++++++++++++++-- .../Client/Native/FesBlob.cs | 198 +++++++++++++-- .../Client/Native/IFbClient.cs | 21 +- .../Common/BlobBase.cs | 41 +-- .../Common/BlobStream.cs | 230 +++++++++++++++++ .../Common/DbValue.cs | 27 ++ .../Common/IscCodes.cs | 4 + .../FirebirdClient/FbDataReader.cs | 10 + 10 files changed, 796 insertions(+), 55 deletions(-) create mode 100644 src/FirebirdSql.Data.FirebirdClient.Tests/BlobStreamTests.cs create mode 100644 src/FirebirdSql.Data.FirebirdClient/Common/BlobStream.cs diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/BlobStreamTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/BlobStreamTests.cs new file mode 100644 index 00000000..6631ff77 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/BlobStreamTests.cs @@ -0,0 +1,86 @@ +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using FirebirdSql.Data.TestsBase; +using NUnit.Framework; + +namespace FirebirdSql.Data.FirebirdClient.Tests; + +[TestFixtureSource(typeof(FbServerTypeTestFixtureSource), nameof(FbServerTypeTestFixtureSource.Default))] +[TestFixtureSource(typeof(FbServerTypeTestFixtureSource), nameof(FbServerTypeTestFixtureSource.Embedded))] +public class BlobStreamTests : FbTestsBase +{ + public BlobStreamTests(FbServerType serverType, bool compression, FbWireCrypt wireCrypt) + : base(serverType, compression, wireCrypt) + { } + + [Test] + public async Task FbBlobStreamReadTest() + { + var id_value = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue); + var insert_values = RandomNumberGenerator.GetBytes(100000 * 4); + + await using (var transaction = await Connection.BeginTransactionAsync()) + { + await using (var insert = new FbCommand("INSERT INTO TEST (int_field, blob_field) values(@int_field, @blob_field)", Connection, transaction)) + { + insert.Parameters.Add("@int_field", FbDbType.Integer).Value = id_value; + insert.Parameters.Add("@blob_field", FbDbType.Binary).Value = insert_values; + await insert.ExecuteNonQueryAsync(); + } + await transaction.CommitAsync(); + } + + await using (var select = new FbCommand($"SELECT blob_field FROM TEST WHERE int_field = {id_value}", Connection)) + { + await using var reader = await select.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + await using var output = new MemoryStream(); + await using (var stream = reader.GetStream(0)) + { + await stream.CopyToAsync(output); + } + + var select_values = output.ToArray(); + CollectionAssert.AreEqual(insert_values, select_values); + } + } + } + + [Test] + public async Task FbBlobStreamWriteTest() + { + var id_value = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue); + var insert_values = RandomNumberGenerator.GetBytes(100000 * 4); + + await using (var transaction = await Connection.BeginTransactionAsync()) + { + await using (var insert = new FbCommand("INSERT INTO TEST (int_field, blob_field) values(@int_field, @blob_field)", Connection, transaction)) + { + insert.Parameters.Add("@int_field", FbDbType.Integer).Value = id_value; + insert.Parameters.Add("@blob_field", FbDbType.Binary).Value = insert_values; + await insert.ExecuteNonQueryAsync(); + } + + await using (var select = new FbCommand($"SELECT blob_field FROM TEST WHERE int_field = {id_value}", Connection, transaction)) + { + await using var reader = await select.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + await using var stream = reader.GetStream(0); + await stream.WriteAsync(insert_values); + + break; + } + } + await transaction.CommitAsync(); + } + + await using (var select = new FbCommand($"SELECT blob_field FROM TEST WHERE int_field = {id_value}", Connection)) + { + var select_values = (byte[])await select.ExecuteScalarAsync(); + CollectionAssert.AreEqual(insert_values, select_values); + } + } +} \ No newline at end of file diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbBlobTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbBlobTests.cs index 61537d06..d81d3a3e 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbBlobTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbBlobTests.cs @@ -44,6 +44,7 @@ public async Task BinaryBlobTest() insert.Parameters.Add("@blob_field", FbDbType.Binary).Value = insert_values; await insert.ExecuteNonQueryAsync(); } + await transaction.CommitAsync(); } diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs index 0b5fc37e..8436eece 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs @@ -69,7 +69,7 @@ public GdsBlob(GdsDatabase database, GdsTransaction transaction, long blobId) #region Protected Methods - protected override void Create() + public override void Create() { try { @@ -81,7 +81,7 @@ protected override void Create() throw; } } - protected override async ValueTask CreateAsync(CancellationToken cancellationToken = default) + public override async ValueTask CreateAsync(CancellationToken cancellationToken = default) { try { @@ -94,7 +94,7 @@ protected override async ValueTask CreateAsync(CancellationToken cancellationTok } } - protected override void Open() + public override void Open() { try { @@ -105,7 +105,7 @@ protected override void Open() throw; } } - protected override async ValueTask OpenAsync(CancellationToken cancellationToken = default) + public override async ValueTask OpenAsync(CancellationToken cancellationToken = default) { try { @@ -117,7 +117,87 @@ protected override async ValueTask OpenAsync(CancellationToken cancellationToken } } - protected override void GetSegment(Stream stream) + public override int GetLength() + { + try + { + if (!IsOpen) + Open(); + + var bufferLength = 20; + var buffer = new byte[bufferLength]; + + _database.Xdr.Write(IscCodes.op_info_blob); + _database.Xdr.Write(_blobHandle); + _database.Xdr.Write(0); + _database.Xdr.WriteBuffer(new byte[] { IscCodes.isc_info_blob_total_length }, 1); + _database.Xdr.Write(bufferLength); + + _database.Xdr.Flush(); + + var response = (GenericResponse)_database.ReadResponse(); + + var responseLength = bufferLength; + + if (response.Data.Length < bufferLength) + { + responseLength = response.Data.Length; + } + + Buffer.BlockCopy(response.Data, 0, buffer, 0, responseLength); + + var length = IscHelper.VaxInteger(buffer, 1, 2); + var size = IscHelper.VaxInteger(buffer, 3, (int)length); + + return (int)size; + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + } + + public override async ValueTask GetLengthAsync(CancellationToken cancellationToken = default) + { + try + { + if (!IsOpen) + await OpenAsync(cancellationToken).ConfigureAwait(false); + + var bufferLength = 20; + var buffer = new byte[bufferLength]; + + await _database.Xdr.WriteAsync(IscCodes.op_info_blob, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(_blobHandle, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(0, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteBufferAsync(new byte[] { IscCodes.isc_info_blob_total_length }, 1, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(bufferLength, cancellationToken).ConfigureAwait(false); + + await _database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); + + var response = (GenericResponse)await _database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); + + var responseLength = bufferLength; + + if (response.Data.Length < bufferLength) + { + responseLength = response.Data.Length; + } + + Buffer.BlockCopy(response.Data, 0, buffer, 0, responseLength); + + var length = IscHelper.VaxInteger(buffer, 1, 2); + var size = IscHelper.VaxInteger(buffer, 3, (int)length); + + return (int)size; + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + } + + public override void GetSegment(Stream stream) { var requested = SegmentSize; @@ -166,7 +246,7 @@ protected override void GetSegment(Stream stream) throw IscException.ForIOException(ex); } } - protected override async ValueTask GetSegmentAsync(Stream stream, CancellationToken cancellationToken = default) + public override async ValueTask GetSegmentAsync(Stream stream, CancellationToken cancellationToken = default) { var requested = SegmentSize; @@ -194,7 +274,7 @@ protected override async ValueTask GetSegmentAsync(Stream stream, CancellationTo if (buffer.Length == 0) { - // previous segment was last, this has no data + //previous segment was last, this has no data return; } @@ -216,7 +296,120 @@ protected override async ValueTask GetSegmentAsync(Stream stream, CancellationTo } } - protected override void PutSegment(byte[] buffer) + public override byte[] GetSegment() + { + var requested = SegmentSize; + + try + { + _database.Xdr.Write(IscCodes.op_get_segment); + _database.Xdr.Write(_blobHandle); + _database.Xdr.Write(requested < short.MaxValue - 12 ? requested : short.MaxValue - 12); + _database.Xdr.Write(DataSegment); + _database.Xdr.Flush(); + + var response = (GenericResponse)_database.ReadResponse(); + + RblRemoveValue(IscCodes.RBL_segment); + if (response.ObjectHandle == 1) + { + RblAddValue(IscCodes.RBL_segment); + } + else if (response.ObjectHandle == 2) + { + RblAddValue(IscCodes.RBL_eof_pending); + } + + var buffer = response.Data; + + if (buffer.Length == 0) + { + //previous segment was last, this has no data + return Array.Empty(); + } + + var posInInput = 0; + var posInOutput = 0; + + var tmp = new byte[requested * 2]; + while (posInInput < buffer.Length) + { + var len = (int)IscHelper.VaxInteger(buffer, posInInput, 2); + posInInput += 2; + + Array.Copy(buffer, posInInput, tmp, posInOutput, len); + posInOutput += len; + posInInput += len; + } + + var actualBuffer = new byte[posInOutput]; + Array.Copy(tmp, actualBuffer, posInOutput); + + return actualBuffer; + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + } + public override async ValueTask GetSegmentAsync(CancellationToken cancellationToken = default) + { + var requested = SegmentSize; + + try + { + await _database.Xdr.WriteAsync(IscCodes.op_get_segment, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(_blobHandle, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(requested < short.MaxValue - 12 ? requested : short.MaxValue - 12, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(DataSegment, cancellationToken).ConfigureAwait(false); + await _database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); + + var response = (GenericResponse)await _database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); + + RblRemoveValue(IscCodes.RBL_segment); + if (response.ObjectHandle == 1) + { + RblAddValue(IscCodes.RBL_segment); + } + else if (response.ObjectHandle == 2) + { + RblAddValue(IscCodes.RBL_eof_pending); + } + + var buffer = response.Data; + + if (buffer.Length == 0) + { + // previous segment was last, this has no data + return Array.Empty(); + } + + var posInInput = 0; + var posInOutput = 0; + + var tmp = new byte[requested * 2]; + while (posInInput < buffer.Length) + { + var len = (int)IscHelper.VaxInteger(buffer, posInInput, 2); + posInInput += 2; + + Array.Copy(buffer, posInInput, tmp, posInOutput, len); + posInOutput += len; + posInInput += len; + } + + var actualBuffer = new byte[posInOutput]; + Array.Copy(tmp, actualBuffer, posInOutput); + + return actualBuffer; + } + catch (IOException ex) + { + throw IscException.ForIOException(ex); + } + } + + public override void PutSegment(byte[] buffer) { try { @@ -232,7 +425,7 @@ protected override void PutSegment(byte[] buffer) throw IscException.ForIOException(ex); } } - protected override async ValueTask PutSegmentAsync(byte[] buffer, CancellationToken cancellationToken = default) + public override async ValueTask PutSegmentAsync(byte[] buffer, CancellationToken cancellationToken = default) { try { @@ -249,14 +442,14 @@ protected override async ValueTask PutSegmentAsync(byte[] buffer, CancellationTo } } - protected override void Seek(int position) + public override void Seek(int offset, int seekMode) { try { _database.Xdr.Write(IscCodes.op_seek_blob); _database.Xdr.Write(_blobHandle); - _database.Xdr.Write(SeekMode); - _database.Xdr.Write(position); + _database.Xdr.Write(seekMode); + _database.Xdr.Write(offset); _database.Xdr.Flush(); var response = (GenericResponse)_database.ReadResponse(); @@ -268,14 +461,14 @@ protected override void Seek(int position) throw IscException.ForIOException(ex); } } - protected override async ValueTask SeekAsync(int position, CancellationToken cancellationToken = default) + public override async ValueTask SeekAsync(int offset, int seekMode, CancellationToken cancellationToken = default) { try { await _database.Xdr.WriteAsync(IscCodes.op_seek_blob, cancellationToken).ConfigureAwait(false); await _database.Xdr.WriteAsync(_blobHandle, cancellationToken).ConfigureAwait(false); - await _database.Xdr.WriteAsync(SeekMode, cancellationToken).ConfigureAwait(false); - await _database.Xdr.WriteAsync(position, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(seekMode, cancellationToken).ConfigureAwait(false); + await _database.Xdr.WriteAsync(offset, cancellationToken).ConfigureAwait(false); await _database.Xdr.FlushAsync(cancellationToken).ConfigureAwait(false); var response = (GenericResponse)await _database.ReadResponseAsync(cancellationToken).ConfigureAwait(false); @@ -288,20 +481,20 @@ protected override async ValueTask SeekAsync(int position, CancellationToken can } } - protected override void Close() + public override void Close() { _database.ReleaseObject(IscCodes.op_close_blob, _blobHandle); } - protected override ValueTask CloseAsync(CancellationToken cancellationToken = default) + public override ValueTask CloseAsync(CancellationToken cancellationToken = default) { return _database.ReleaseObjectAsync(IscCodes.op_close_blob, _blobHandle, cancellationToken); } - protected override void Cancel() + public override void Cancel() { _database.ReleaseObject(IscCodes.op_cancel_blob, _blobHandle); } - protected override ValueTask CancelAsync(CancellationToken cancellationToken = default) + public override ValueTask CancelAsync(CancellationToken cancellationToken = default) { return _database.ReleaseObjectAsync(IscCodes.op_cancel_blob, _blobHandle, cancellationToken); } @@ -327,6 +520,7 @@ private void CreateOrOpen(int op, BlobParameterBuffer bpb) _blobId = response.BlobId; _blobHandle = response.ObjectHandle; + _isOpen = true; } catch (IOException ex) { @@ -350,6 +544,7 @@ private async ValueTask CreateOrOpenAsync(int op, BlobParameterBuffer bpb, Cance _blobId = response.BlobId; _blobHandle = response.ObjectHandle; + _isOpen = true; } catch (IOException ex) { diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesBlob.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesBlob.cs index 6ca97a51..b363b5b3 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesBlob.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Native/FesBlob.cs @@ -70,7 +70,7 @@ public FesBlob(FesDatabase database, FesTransaction transaction, long blobId) #region Protected Methods - protected override void Create() + public override void Create() { ClearStatusVector(); @@ -88,9 +88,11 @@ protected override void Create() _database.ProcessStatusVector(_statusVector); + _isOpen = true; + RblAddValue(IscCodes.RBL_create); } - protected override ValueTask CreateAsync(CancellationToken cancellationToken = default) + public override ValueTask CreateAsync(CancellationToken cancellationToken = default) { ClearStatusVector(); @@ -108,12 +110,14 @@ protected override ValueTask CreateAsync(CancellationToken cancellationToken = d _database.ProcessStatusVector(_statusVector); + _isOpen = true; + RblAddValue(IscCodes.RBL_create); return ValueTask2.CompletedTask; } - protected override void Open() + public override void Open() { ClearStatusVector(); @@ -130,8 +134,10 @@ protected override void Open() new byte[0]); _database.ProcessStatusVector(_statusVector); + + _isOpen = true; } - protected override ValueTask OpenAsync(CancellationToken cancellationToken = default) + public override ValueTask OpenAsync(CancellationToken cancellationToken = default) { ClearStatusVector(); @@ -149,10 +155,56 @@ protected override ValueTask OpenAsync(CancellationToken cancellationToken = def _database.ProcessStatusVector(_statusVector); + _isOpen = true; + return ValueTask2.CompletedTask; } - protected override void GetSegment(Stream stream) + public override int GetLength() + { + ClearStatusVector(); + + var buffer = new byte[20]; + + _database.FbClient.isc_blob_info( + _statusVector, + ref _blobHandle, + 1, + new byte[] { IscCodes.isc_info_blob_total_length }, + (short)buffer.Length, + buffer); + + _database.ProcessStatusVector(_statusVector); + + var length = IscHelper.VaxInteger(buffer, 1, 2); + var size = IscHelper.VaxInteger(buffer, 3, (int)length); + + return (int)size; + } + + public override ValueTask GetLengthAsync(CancellationToken cancellationToken = default) + { + ClearStatusVector(); + + var buffer = new byte[20]; + + _database.FbClient.isc_blob_info( + _statusVector, + ref _blobHandle, + 1, + new byte[] { IscCodes.isc_info_blob_total_length }, + (short)buffer.Length, + buffer); + + _database.ProcessStatusVector(_statusVector); + + var length = IscHelper.VaxInteger(buffer, 1, 2); + var size = IscHelper.VaxInteger(buffer, 3, (int)length); + + return ValueTask2.FromResult((int)size); + } + + public override void GetSegment(Stream stream) { var requested = (short)SegmentSize; short segmentLength = 0; @@ -168,7 +220,6 @@ protected override void GetSegment(Stream stream) requested, tmp); - RblRemoveValue(IscCodes.RBL_segment); if (_statusVector[1] == new IntPtr(IscCodes.isc_segstr_eof)) @@ -190,7 +241,7 @@ protected override void GetSegment(Stream stream) stream.Write(tmp, 0, segmentLength); } - protected override ValueTask GetSegmentAsync(Stream stream, CancellationToken cancellationToken = default) + public override ValueTask GetSegmentAsync(Stream stream, CancellationToken cancellationToken = default) { var requested = (short)SegmentSize; short segmentLength = 0; @@ -231,7 +282,98 @@ protected override ValueTask GetSegmentAsync(Stream stream, CancellationToken ca return ValueTask2.CompletedTask; } - protected override void PutSegment(byte[] buffer) + public override byte[] GetSegment() + { + var requested = (short)(SegmentSize - 2); + short segmentLength = 0; + + ClearStatusVector(); + + var tmp = new byte[requested]; + + var status = _database.FbClient.isc_get_segment( + _statusVector, + ref _blobHandle, + ref segmentLength, + requested, + tmp); + + + RblRemoveValue(IscCodes.RBL_segment); + + if (_statusVector[1] == new IntPtr(IscCodes.isc_segstr_eof)) + { + RblAddValue(IscCodes.RBL_eof_pending); + return Array.Empty(); + } + + if (status == IntPtr.Zero || _statusVector[1] == new IntPtr(IscCodes.isc_segment)) + { + RblAddValue(IscCodes.RBL_segment); + } + else + { + _database.ProcessStatusVector(_statusVector); + } + + var actualSegment = tmp; + if (actualSegment.Length != segmentLength) + { + tmp = new byte[segmentLength]; + Array.Copy(actualSegment, tmp, segmentLength); + actualSegment = tmp; + } + + return actualSegment; + } + public override ValueTask GetSegmentAsync(CancellationToken cancellationToken = default) + { + var requested = (short)SegmentSize; + short segmentLength = 0; + + ClearStatusVector(); + + var tmp = new byte[requested]; + + var status = _database.FbClient.isc_get_segment( + _statusVector, + ref _blobHandle, + ref segmentLength, + requested, + tmp); + + + RblRemoveValue(IscCodes.RBL_segment); + + if (_statusVector[1] == new IntPtr(IscCodes.isc_segstr_eof)) + { + RblAddValue(IscCodes.RBL_eof_pending); + return ValueTask2.FromResult(Array.Empty()); + } + else + { + if (status == IntPtr.Zero || _statusVector[1] == new IntPtr(IscCodes.isc_segment)) + { + RblAddValue(IscCodes.RBL_segment); + } + else + { + _database.ProcessStatusVector(_statusVector); + } + } + + var actualSegment = tmp; + if (actualSegment.Length != segmentLength) + { + tmp = new byte[segmentLength]; + Array.Copy(actualSegment, tmp, segmentLength); + actualSegment = tmp; + } + + return ValueTask2.FromResult(actualSegment); + } + + public override void PutSegment(byte[] buffer) { ClearStatusVector(); @@ -243,7 +385,7 @@ protected override void PutSegment(byte[] buffer) _database.ProcessStatusVector(_statusVector); } - protected override ValueTask PutSegmentAsync(byte[] buffer, CancellationToken cancellationToken = default) + public override ValueTask PutSegmentAsync(byte[] buffer, CancellationToken cancellationToken = default) { ClearStatusVector(); @@ -258,16 +400,38 @@ protected override ValueTask PutSegmentAsync(byte[] buffer, CancellationToken ca return ValueTask2.CompletedTask; } - protected override void Seek(int position) + public override void Seek(int position, int seekOperation) { - throw new NotSupportedException(); + ClearStatusVector(); + + var resultingPosition = 0; + _database.FbClient.isc_seek_blob( + _statusVector, + ref _blobHandle, + (short)seekOperation, + position, + ref resultingPosition); + + _database.ProcessStatusVector(_statusVector); } - protected override ValueTask SeekAsync(int position, CancellationToken cancellationToken = default) + public override ValueTask SeekAsync(int position, int seekOperation, CancellationToken cancellationToken = default) { - throw new NotSupportedException(); + ClearStatusVector(); + + var resultingPosition = 0; + _database.FbClient.isc_seek_blob( + _statusVector, + ref _blobHandle, + (short)seekOperation, + position, + ref resultingPosition); + + _database.ProcessStatusVector(_statusVector); + + return ValueTask2.CompletedTask; } - protected override void Close() + public override void Close() { ClearStatusVector(); @@ -275,7 +439,7 @@ protected override void Close() _database.ProcessStatusVector(_statusVector); } - protected override ValueTask CloseAsync(CancellationToken cancellationToken = default) + public override ValueTask CloseAsync(CancellationToken cancellationToken = default) { ClearStatusVector(); @@ -286,7 +450,7 @@ protected override ValueTask CloseAsync(CancellationToken cancellationToken = de return ValueTask2.CompletedTask; } - protected override void Cancel() + public override void Cancel() { ClearStatusVector(); @@ -294,7 +458,7 @@ protected override void Cancel() _database.ProcessStatusVector(_statusVector); } - protected override ValueTask CancelAsync(CancellationToken cancellationToken = default) + public override ValueTask CancelAsync(CancellationToken cancellationToken = default) { ClearStatusVector(); diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Native/IFbClient.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Native/IFbClient.cs index 15aa3148..f49b09f0 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Native/IFbClient.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Native/IFbClient.cs @@ -23,8 +23,8 @@ namespace FirebirdSql.Data.Client.Native; /// -/// This is the interface that the dynamically-generated class uses to call the native library. -/// Each connection can specify different client library to use even on the same OS. +/// This is the interface that the dynamically-generated class uses to call the native library. +/// Each connection can specify different client library to use even on the same OS. /// IFbClient and FbClientactory classes are implemented to support this feature. /// Public visibility added, because auto-generated assembly can't work with internal types /// @@ -68,6 +68,14 @@ IntPtr isc_open_blob2( short bpbLength, byte[] bpbAddress); + IntPtr isc_blob_info( + [In, Out] IntPtr[] statusVector, + ref BlobHandle blobHandle, + short itemListBufferLength, + byte[] itemListBuffer, + short resultBufferLength, + byte[] resultBuffer); + IntPtr isc_get_segment( [In, Out] IntPtr[] statusVector, [MarshalAs(UnmanagedType.I4)] ref BlobHandle blobHandle, @@ -81,6 +89,13 @@ IntPtr isc_put_segment( short segBufferLength, byte[] segBuffer); + IntPtr isc_seek_blob( + [In, Out] IntPtr[] statusVector, + [MarshalAs(UnmanagedType.I4)] ref BlobHandle blobHandle, + short mode, + int offset, + ref int resultingBlobPosition); + IntPtr isc_cancel_blob( [In, Out] IntPtr[] statusVector, [MarshalAs(UnmanagedType.I4)] ref BlobHandle blobHandle); @@ -260,4 +275,4 @@ IntPtr isc_transaction_info( byte[] resultBuffer); #pragma warning restore IDE1006 -} +} \ No newline at end of file diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/BlobBase.cs b/src/FirebirdSql.Data.FirebirdClient/Common/BlobBase.cs index 828b80c2..7214d5e8 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/BlobBase.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/BlobBase.cs @@ -29,14 +29,17 @@ internal abstract class BlobBase private int _segmentSize; protected long _blobId; + protected bool _isOpen; protected int _position; protected TransactionBase _transaction; public abstract int Handle { get; } public long Id => _blobId; public bool EOF => (_rblFlags & IscCodes.RBL_eof_pending) != 0; + public bool IsOpen => _isOpen; - protected int SegmentSize => _segmentSize; + public int SegmentSize => _segmentSize; + public int Position => _position; public abstract DatabaseBase Database { get; } @@ -203,26 +206,32 @@ public async ValueTask WriteAsync(byte[] buffer, int index, int count, Cancellat } } - protected abstract void Create(); - protected abstract ValueTask CreateAsync(CancellationToken cancellationToken = default); + public abstract void Create(); + public abstract ValueTask CreateAsync(CancellationToken cancellationToken = default); - protected abstract void Open(); - protected abstract ValueTask OpenAsync(CancellationToken cancellationToken = default); + public abstract void Open(); + public abstract ValueTask OpenAsync(CancellationToken cancellationToken = default); - protected abstract void GetSegment(Stream stream); - protected abstract ValueTask GetSegmentAsync(Stream stream, CancellationToken cancellationToken = default); + public abstract int GetLength(); + public abstract ValueTask GetLengthAsync(CancellationToken cancellationToken = default); - protected abstract void PutSegment(byte[] buffer); - protected abstract ValueTask PutSegmentAsync(byte[] buffer, CancellationToken cancellationToken = default); + public abstract byte[] GetSegment(); + public abstract ValueTask GetSegmentAsync(CancellationToken cancellationToken = default); - protected abstract void Seek(int position); - protected abstract ValueTask SeekAsync(int position, CancellationToken cancellationToken = default); + public abstract void GetSegment(Stream stream); + public abstract ValueTask GetSegmentAsync(Stream stream, CancellationToken cancellationToken = default); - protected abstract void Close(); - protected abstract ValueTask CloseAsync(CancellationToken cancellationToken = default); + public abstract void PutSegment(byte[] buffer); + public abstract ValueTask PutSegmentAsync(byte[] buffer, CancellationToken cancellationToken = default); - protected abstract void Cancel(); - protected abstract ValueTask CancelAsync(CancellationToken cancellationToken = default); + public abstract void Seek(int offset, int seekMode); + public abstract ValueTask SeekAsync(int offset, int seekMode, CancellationToken cancellationToken = default); + + public abstract void Close(); + public abstract ValueTask CloseAsync(CancellationToken cancellationToken = default); + + public abstract void Cancel(); + public abstract ValueTask CancelAsync(CancellationToken cancellationToken = default); protected void RblAddValue(int rblValue) { @@ -233,4 +242,4 @@ protected void RblRemoveValue(int rblValue) { _rblFlags &= ~rblValue; } -} +} \ No newline at end of file diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/BlobStream.cs b/src/FirebirdSql.Data.FirebirdClient/Common/BlobStream.cs new file mode 100644 index 00000000..050873a7 --- /dev/null +++ b/src/FirebirdSql.Data.FirebirdClient/Common/BlobStream.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace FirebirdSql.Data.Common; + +public sealed class BlobStream : Stream +{ + private readonly BlobBase _blobHandle; + private int _position; + + private byte[] _currentSegment; + private int _segmentPosition; + + private int Available => _currentSegment?.Length - _segmentPosition ?? 0; + + internal BlobStream(BlobBase blob) + { + _blobHandle = blob; + _position = 0; + } + + public override long Position + { + get => _position; + set => Seek(value, SeekOrigin.Begin); + } + + public override long Length + { + get + { + if (!_blobHandle.IsOpen) + _blobHandle.Open(); + + return _blobHandle.GetLength(); + } + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferSize(buffer, offset, count); + + if (!_blobHandle.IsOpen) + _blobHandle.Open(); + + var copied = 0; + var remainingBufferSize = buffer.Length - offset; + do + { + if (remainingBufferSize == 0) + break; + + if (Available > 0) + { + var toCopy = Math.Min(Available, remainingBufferSize); + Array.Copy(_currentSegment, _segmentPosition, buffer, offset + copied, toCopy); + copied += toCopy; + _segmentPosition += toCopy; + remainingBufferSize -= toCopy; + _position += toCopy; + } + + if (_blobHandle.EOF) + break; + + if (Available == 0) + { + _currentSegment = _blobHandle.GetSegment(); + _segmentPosition = 0; + } + } while (copied < count); + + return copied; + } + public override async Task ReadAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + ValidateBufferSize(buffer, offset, count); + + if (!_blobHandle.IsOpen) + await _blobHandle.OpenAsync(cancellationToken).ConfigureAwait(false); + + var copied = 0; + var remainingBufferSize = buffer.Length - offset; + do + { + if (remainingBufferSize == 0) + break; + + if (Available > 0) + { + var toCopy = Math.Min(Available, remainingBufferSize); + Array.Copy(_currentSegment, _segmentPosition, buffer, offset + copied, toCopy); + copied += toCopy; + _segmentPosition += toCopy; + remainingBufferSize -= toCopy; + _position += toCopy; + } + + if (_blobHandle.EOF) + break; + + if (Available == 0) + { + _currentSegment = await _blobHandle.GetSegmentAsync(cancellationToken).ConfigureAwait(false); + _segmentPosition = 0; + } + } while (copied < count); + + return copied; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (!_blobHandle.IsOpen) + _blobHandle.Open(); + + var seekMode = origin switch + { + SeekOrigin.Begin => IscCodes.isc_blb_seek_from_head, + SeekOrigin.Current => IscCodes.isc_blb_seek_relative, + SeekOrigin.End => IscCodes.isc_blb_seek_from_tail, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; + + _blobHandle.Seek((int)offset, seekMode); + return _position = _blobHandle.Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + try + { + if (!_blobHandle.IsOpen) + _blobHandle.Create(); + + var chunk = count >= _blobHandle.SegmentSize ? _blobHandle.SegmentSize : count; + var tmpBuffer = new byte[chunk]; + + while (count > 0) + { + if (chunk > count) + { + chunk = count; + tmpBuffer = new byte[chunk]; + } + + Array.Copy(buffer, offset, tmpBuffer, 0, chunk); + _blobHandle.PutSegment(tmpBuffer); + + offset += chunk; + count -= chunk; + _position += chunk; + } + } + catch + { + _blobHandle.Cancel(); + throw; + } + } + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + try + { + if (!_blobHandle.IsOpen) + await _blobHandle.CreateAsync(cancellationToken).ConfigureAwait(false); + + var chunk = count >= _blobHandle.SegmentSize ? _blobHandle.SegmentSize : count; + var tmpBuffer = new byte[chunk]; + + while (count > 0) + { + if (chunk > count) + { + chunk = count; + tmpBuffer = new byte[chunk]; + } + + Array.Copy(buffer, offset, tmpBuffer, 0, chunk); + await _blobHandle.PutSegmentAsync(tmpBuffer, cancellationToken).ConfigureAwait(false); + + offset += chunk; + count -= chunk; + _position += chunk; + } + } + catch + { + await _blobHandle.CancelAsync(cancellationToken).ConfigureAwait(false); + throw; + } + } + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => true; + + protected override void Dispose(bool disposing) + { + _blobHandle.Close(); + } + +#if !(NET48 || NETSTANDARD2_0) + public override ValueTask DisposeAsync() + { + return _blobHandle.CloseAsync(); + } +#endif + + private static void ValidateBufferSize(byte[] buffer, int offset, int count) + { + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + + if (buffer.Length < offset + count) + throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs index f799b5ac..b0656d3e 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/DbValue.cs @@ -20,6 +20,7 @@ using System.Numerics; using System.Threading; using System.Threading.Tasks; +using FirebirdSql.Data.FirebirdClient; using FirebirdSql.Data.Types; namespace FirebirdSql.Data.Common; @@ -331,6 +332,21 @@ public async ValueTask GetBinaryAsync(CancellationToken cancellationToke return (byte[])_value; } + public BlobStream GetBinaryStream() + { + if (_value is not long l) + throw new NotSupportedException(); + + return GetBlobStream(l); + } + public ValueTask GetBinaryStreamAsync(CancellationToken cancellationToken = default) + { + if (_value is not long l) + throw new NotSupportedException(); + + return GetBlobStreamAsync(l, cancellationToken); + } + public int GetDate() { return _value switch @@ -854,6 +870,17 @@ private ValueTask GetBlobDataAsync(long blobId, CancellationToken cancel return blob.ReadAsync(cancellationToken); } + private BlobStream GetBlobStream(long blobId) + { + var blob = _statement.CreateBlob(blobId); + return new BlobStream(blob); + } + private ValueTask GetBlobStreamAsync(long blobId, CancellationToken cancellationToken = default) + { + var blob = _statement.CreateBlob(blobId); + return ValueTask2.FromResult(new BlobStream(blob)); + } + private Array GetArrayData(long handle) { if (_field.ArrayHandle == null) diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs b/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs index 40bb78da..25a9d29a 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/IscCodes.cs @@ -941,6 +941,10 @@ internal static class IscCodes public const int RBL_eof_pending = 4; public const int RBL_create = 8; + public const int isc_blb_seek_from_head = 0; + public const int isc_blb_seek_relative = 1; + public const int isc_blb_seek_from_tail = 2; + #endregion #region Blob Information diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs index 7972ee04..de8cebb2 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbDataReader.cs @@ -21,6 +21,7 @@ using System.ComponentModel; using System.Data; using System.Data.Common; +using System.IO; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -978,6 +979,15 @@ public override DateTime GetDateTime(int i) return GetFieldValue(i); } + public override Stream GetStream(int i) + { + CheckState(); + CheckPosition(); + CheckIndex(i); + + return _row[i].GetBinaryStream(); + } + public override bool IsDBNull(int i) { CheckState(); From ba37f816673ab032f800a6fee6d56abc32f153b7 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 12 Nov 2024 13:43:41 +0100 Subject: [PATCH 05/11] Fix comments --- .../Client/Managed/Version10/GdsBlob.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs index 8436eece..7d678514 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Client/Managed/Version10/GdsBlob.cs @@ -225,7 +225,7 @@ public override void GetSegment(Stream stream) if (buffer.Length == 0) { - // previous segment was last, this has no data + // previous segment was last, this has no data return; } @@ -380,7 +380,7 @@ public override async ValueTask GetSegmentAsync(CancellationToken cancel if (buffer.Length == 0) { - // previous segment was last, this has no data + // previous segment was last, this has no data return Array.Empty(); } From e7e60492e00439733442866fd3e57dafe82f12bc Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 10 Dec 2024 10:44:01 +0100 Subject: [PATCH 06/11] Update BDN package. --- src/Perf/Perf.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perf/Perf.csproj b/src/Perf/Perf.csproj index 3aa612b2..3896df31 100644 --- a/src/Perf/Perf.csproj +++ b/src/Perf/Perf.csproj @@ -5,7 +5,7 @@ true - + From c481620d90e5c4b63a4d144b8cd06668e3688425 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 10 Dec 2024 10:50:27 +0100 Subject: [PATCH 07/11] Update System.Threading.Tasks.Extensions. --- .../FirebirdSql.Data.FirebirdClient.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj index afc84988..bd28a79c 100644 --- a/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj +++ b/src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj @@ -40,11 +40,11 @@ - + - + From e439935c74bde1b94e05ce6c8fb6fcdb1ac3dcdf Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 10 Dec 2024 10:44:37 +0100 Subject: [PATCH 08/11] Update testing packages. --- .../EntityFramework.Firebird.Tests.csproj | 13 +++++++++---- .../FirebirdSql.Data.FirebirdClient.Tests.csproj | 13 +++++++++---- ...ityFrameworkCore.Firebird.FunctionalTests.csproj | 2 +- ...irdSql.EntityFrameworkCore.Firebird.Tests.csproj | 13 +++++++++---- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/EntityFramework.Firebird.Tests/EntityFramework.Firebird.Tests.csproj b/src/EntityFramework.Firebird.Tests/EntityFramework.Firebird.Tests.csproj index 52ce2002..52fbd139 100644 --- a/src/EntityFramework.Firebird.Tests/EntityFramework.Firebird.Tests.csproj +++ b/src/EntityFramework.Firebird.Tests/EntityFramework.Firebird.Tests.csproj @@ -11,15 +11,20 @@ Exe FirebirdSql.Data.TestsBase.Program + + + + + - - - - + + + + diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FirebirdSql.Data.FirebirdClient.Tests.csproj b/src/FirebirdSql.Data.FirebirdClient.Tests/FirebirdSql.Data.FirebirdClient.Tests.csproj index 8f2fc2b5..1e902480 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FirebirdSql.Data.FirebirdClient.Tests.csproj +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FirebirdSql.Data.FirebirdClient.Tests.csproj @@ -12,10 +12,15 @@ FirebirdSql.Data.TestsBase.Program - - - - + + + + + + + + + diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.csproj b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.csproj index bbadea15..df649fe4 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.csproj +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests/FirebirdSql.EntityFrameworkCore.Firebird.FunctionalTests.csproj @@ -12,7 +12,7 @@ - + all diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj index ce3ed3f3..851bf975 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj @@ -12,10 +12,15 @@ FirebirdSql.Data.TestsBase.Program - - - - + + + + + + + + + From 10517b38ddc9619c04352dcc2fb054f443605b8c Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 10 Dec 2024 11:47:47 +0100 Subject: [PATCH 09/11] Bump version. --- src/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Versions.props b/src/Versions.props index bb8ae97e..0a712c70 100644 --- a/src/Versions.props +++ b/src/Versions.props @@ -1,7 +1,7 @@ - 10.3.1 + 10.3.2 From 9bc4e30b083c600416a16294a324ff91bad9c044 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 10 Dec 2024 10:53:46 +0100 Subject: [PATCH 10/11] Update EF6 stuff. --- src/Versions.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Versions.props b/src/Versions.props index 0a712c70..1c034683 100644 --- a/src/Versions.props +++ b/src/Versions.props @@ -11,8 +11,8 @@ - 10.0.1 - 10.1.0 - 6.4.4 + 10.1.0 + 10.3.2 + 6.5.1 From 59cfb1f71ce4832b8f815a6189138d5564de48ab Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Wed, 8 Jan 2025 14:13:21 +0100 Subject: [PATCH 11/11] Use Convert.ToHexString. --- .../FbDataReaderTests.cs | 2 +- src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs index 1aa2e50e..c87c804b 100644 --- a/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs +++ b/src/FirebirdSql.Data.FirebirdClient.Tests/FbDataReaderTests.cs @@ -313,7 +313,7 @@ public async Task ReadBinaryTest() { bytes[i] = (byte)random.Next(byte.MinValue, byte.MaxValue); } - var binaryString = $"x'{BitConverter.ToString(bytes).Replace("-", string.Empty)}'"; + var binaryString = $"x'{Convert.ToHexString(bytes)}'"; await using (var command = new FbCommand($"select {binaryString} from TEST", Connection, transaction)) { diff --git a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs index b3d5d432..39bcbd91 100644 --- a/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs +++ b/src/FirebirdSql.Data.FirebirdClient/Common/Extensions.cs @@ -48,7 +48,11 @@ public static IntPtr ReadIntPtr(this BinaryReader self) public static string ToHexString(this byte[] b) { +#if NET5_0_OR_GREATER + return Convert.ToHexString(b); +#else return BitConverter.ToString(b).Replace("-", string.Empty); +#endif } public static IEnumerable> Split(this T[] array, int size)