diff --git a/csharp/client/Dh_NetClient/ReadOnlyListAdapters.cs b/csharp/client/Dh_NetClient/ReadOnlyListAdapters.cs new file mode 100644 index 00000000000..fae45a95621 --- /dev/null +++ b/csharp/client/Dh_NetClient/ReadOnlyListAdapters.cs @@ -0,0 +1,322 @@ +// +// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending +// +using Apache.Arrow; +using System.Collections; +using Array = System.Array; + +namespace Deephaven.Dh_NetClient; + +/// +/// This class hierarchy acts as a wrapper for IReadOnlyList<T> types that allows +/// us to present them simultaneously as various flavors of IList types. We use this +/// to wrap various Arrow array types. The type of wrapping depends on whether we are +/// wrapping an array containing value types or an array containing reference types. +/// +/// Apache.Arrow.StringArray is an example of an array containing reference types. +/// It implements IReadOnlyList<string>. We would wrap it as +/// ReadOnlyListAdapterForReferenceTypes<string> which would provide +/// IList and List<string> to the user. +/// +/// Apache.Arrow.Int32Array is an example of an array containing value types. +/// It implements IReadOnlyList<Nullable<Int32>>. We would wrap it as +/// ReadOnlyListAdapterForValueTypes<Int32> which would provide +/// IList and List<Nullable<Int32>> (via its base class), and +/// also IList<Int32> via the derived class. +/// +/// Background and rationale for this class hierarchy:. The library initially supported a set of +/// scalar types: char, bool, int32, float, string, etc. For each simple type S, there is a +/// ColumnSource<S> that can represent its data and a Chunk<S> that can be used to +/// hold batches of data. When it came time to add support for List types, we had to decide, among +/// other things, what the appropriate ColumnSource and Chunk types would be. This is complicated +/// by the fact that there are infinitely many List types: List can be generic on S, but it can +/// also be generic on List<S> and that can be applied arbitrarily recursively: +/// List<List<S>>, and so on. (However, we are currently only supporting +/// List<S> where S is a scalar type). +/// In terms of representation, we decided that there would be a single ColumnSource<IList> +/// that could represent any list type, and a corresponding Chunk<IList> to be used with it. +/// When programmers work with these IList elements, they can use them as ILists directly, +/// or, to avoid boxing, they can cast them down to their actual concrete type. We promise that +/// the IList elements contained in these ColumnSource and Chunk Types will always implement +/// all ofIList, IList<T> and, for value types, IList<Nullable<T>>. +/// +/// Note: it might have been preferable to use IReadOnlyList<T> and +/// IReadOnlyList<Nullable<T>> instead of IList. The problem is of course that +/// there is no bare (non-generic) IReadOnlyList type, so for the bare type we would still have +/// to use IList. It would have been confusing and inconsistent to use IList for the bare +/// non-generic type, but IReadOnlyList<T> for the generic type.. By using IList we can at least +/// be consistent, even though it's a big interface with a bunch of mutating methods that we don't +/// care about and will always throw exceptions for our case. +/// +public abstract class ReadOnlyListAdapterBase : IList, IList { + protected readonly IReadOnlyList _data; + + public ReadOnlyListAdapterBase(IReadOnlyList data) { + _data = data; + } + + int IList.Add(object? item) => NotImplementedForReadOnlyList(); + void ICollection.Add(T item) => NotImplementedForReadOnlyList(); + + public void Clear() => NotImplementedForReadOnlyList(); + + bool IList.Contains(object? value) => ((IList)this).IndexOf(value) >= 0; + bool ICollection.Contains(T item) => ((IList)this).IndexOf(item) >= 0; + + int IList.IndexOf(object? value) { + for (var i = 0; i != _data.Count; ++i) { + if (Equals(_data[i], value)) { + return i; + } + } + return -1; + } + + int IList.IndexOf(T value) { + for (var i = 0; i != _data.Count; ++i) { + var element = _data[i]; + if (element == null) { + if (value == null) { + return i; + } + continue; + } + if (element.Equals(value)) { + return i; + } + } + return -1; + } + + void IList.Insert(int index, object? value) => NotImplementedForReadOnlyList(); + void IList.Insert(int index, T item) => NotImplementedForReadOnlyList(); + + void IList.Remove(object? value) => NotImplementedForReadOnlyList(); + bool ICollection.Remove(T item) => NotImplementedForReadOnlyList(); + + public void RemoveAt(int index) => NotImplementedForReadOnlyList(); + + bool IList.IsFixedSize => true; + public bool IsReadOnly => true; + + void ICollection.CopyTo(Array array, int index) { + for (var i = 0; i != _data.Count; ++i) { + array.SetValue(_data[i], index + i); + } + } + + void ICollection.CopyTo(T[] array, int arrayIndex) { + for (var i = 0; i != _data.Count; ++i) { + array[arrayIndex + i] = _data[i]; + } + } + + public int Count => _data.Count; + + public bool IsSynchronized => false; + public object SyncRoot => this; + + object? IList.this[int index] { + get { + var value = _data[index]; + if (value == null) { + return null; + } + return value; + } + set => _ = NotImplementedForReadOnlyList(); + } + + T IList.this[int index] { + get => _data[index]; + set => _ = NotImplementedForReadOnlyList(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return _data.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return _data.GetEnumerator(); + } + + protected U NotImplementedForReadOnlyList() { + throw new NotImplementedException("This method is not implemented because the data structure is readonly"); + } +} + +/// +/// This is the leaf class used for wrapping IReadOnlyList<T> where T is a value type. +/// It provides IList and IList<Nullable<T>> to the user via its base class, +/// and provides IList<T> via the derived class. +/// +/// +public sealed class ReadOnlyListAdapterForValueTypes : ReadOnlyListAdapterBase, IList where T : struct, IEquatable { + private readonly T? _deephavenNullValue; + + public ReadOnlyListAdapterForValueTypes(IReadOnlyList data, T? deephavenNullValue) : base(data) { + _deephavenNullValue = deephavenNullValue; + } + + public int IndexOf(T item) { + for (var i = 0; i != _data.Count; ++i) { + var element = StripNull(_data[i]); + if (element.Equals(item)) { + return i; + } + } + return -1; + } + + public void Insert(int index, T item) => _ = NotImplementedForReadOnlyList(); + + public T this[int index] { + get { + var item = _data[index]; + return StripNull(item); + } + set => _ = NotImplementedForReadOnlyList(); + } + + public void Add(T item) => _ = NotImplementedForReadOnlyList(); + + public bool Contains(T item) => IndexOf(item) >= 0; + + public void CopyTo(T[] array, int arrayIndex) { + for (var i = 0; i != Count; ++i) { + array[arrayIndex + i] = this[i]; + } + } + + public bool Remove(T item) => NotImplementedForReadOnlyList(); + + public IEnumerator GetEnumerator() { + foreach (var item in _data) { + yield return StripNull(item); + } + } + + private T StripNull(T? value) { + if (!value.HasValue) { + return _deephavenNullValue ?? + throw new Exception( + $"Assertion failed: This IList contains null value but there is no Deephaven null value for T={Utility.FriendlyTypeName(typeof(T))}. Try casting to IList"); + } + return value.Value; + } +} + +/// +/// This is the leaf class used for wrapping IReadOnlyList<T> where T is a reference type. +/// It provides IList and IList<T> to the user via its base class, +/// +/// +public sealed class ReadOnlyListAdapterForReferenceTypes : ReadOnlyListAdapterBase where T : class { + public ReadOnlyListAdapterForReferenceTypes(IReadOnlyList data) : base(data) { } +} + +public class AdaptorSelector : IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor, + IArrowArrayVisitor { + + public IList Result { get; private set; } = new List(); + + public void Visit(UInt16Array array) { + var adapted = new UInt16ToCharAdaptor(array); + Result = new ReadOnlyListAdapterForValueTypes(adapted, DeephavenConstants.NullChar); + } + + public void Visit(Int8Array array) { + Result = new ReadOnlyListAdapterForValueTypes(array, DeephavenConstants.NullByte); + } + + public void Visit(Int16Array array) { + Result = new ReadOnlyListAdapterForValueTypes(array, DeephavenConstants.NullShort); + } + + public void Visit(Int32Array array) { + Result = new ReadOnlyListAdapterForValueTypes(array, DeephavenConstants.NullInt); + } + + public void Visit(Int64Array array) { + Result = new ReadOnlyListAdapterForValueTypes(array, DeephavenConstants.NullLong); + } + + public void Visit(FloatArray array) { + Result = new ReadOnlyListAdapterForValueTypes(array, DeephavenConstants.NullFloat); + } + + public void Visit(DoubleArray array) { + Result = new ReadOnlyListAdapterForValueTypes(array, DeephavenConstants.NullDouble); + } + + public void Visit(StringArray array) { + Result = new ReadOnlyListAdapterForReferenceTypes(array); + } + + public void Visit(BooleanArray array) { + Result = new ReadOnlyListAdapterForValueTypes(array, null); + } + + public void Visit(TimestampArray array) { + Result = new ReadOnlyListAdapterForValueTypes(array, new DateTimeOffset()); + } + + public void Visit(Date64Array array) { + Result = new ReadOnlyListAdapterForValueTypes(array, new DateOnly()); + } + + public void Visit(Time64Array array) { + Result = new ReadOnlyListAdapterForValueTypes(array, new TimeOnly()); + } + + public void Visit(IArrowArray array) { + throw new NotImplementedException("Client does not support multiple levels of array nesting"); + } +} + +/// +/// "char" support in our system is a special case because it comes in over Arrow as UInt16Array, but +/// we want to present it to users as IList<char>. This class wraps IReadOnlyList<ushort?> +/// and provides IList<char?> to the user. +/// +public class UInt16ToCharAdaptor : IReadOnlyList { + private readonly IReadOnlyList _underlying; + + public UInt16ToCharAdaptor(IReadOnlyList underlying) { + _underlying = underlying; + } + + public char? this[int index] { + get { + var item = _underlying[index]; + return item.HasValue ? (char)item.Value : null; + } + } + + public IEnumerator GetEnumerator() { + foreach (var item in _underlying) { + yield return item.HasValue ? (char)item.Value : null; + } + } + + IEnumerator IEnumerable.GetEnumerator() { + foreach (var item in _underlying) { + yield return item.HasValue ? (char)item.Value : null; + } + } + + public int Count => _underlying.Count; +} + + diff --git a/csharp/client/Dh_NetClient/arrow_util/ArrowArrayConverter.cs b/csharp/client/Dh_NetClient/arrow_util/ArrowArrayConverter.cs index d07864b5ff5..7487a197d34 100644 --- a/csharp/client/Dh_NetClient/arrow_util/ArrowArrayConverter.cs +++ b/csharp/client/Dh_NetClient/arrow_util/ArrowArrayConverter.cs @@ -1,6 +1,8 @@ // // Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending // + +using Apache.Arrow; using Apache.Arrow.Types; namespace Deephaven.Dh_NetClient; @@ -30,21 +32,21 @@ private class ColumnSourceToArrowArrayVisitor : IColumnSourceVisitor, IColumnSourceVisitor, IColumnSourceVisitor, - IColumnSourceVisitor { + IColumnSourceVisitor, + IColumnSourceVisitor { private readonly int _numRows; private readonly Chunk _data; private readonly BooleanChunk _nulls; + public Apache.Arrow.IArrowArray? Result = null; + public ColumnSourceToArrowArrayVisitor(int numRows, Chunk data, BooleanChunk nulls) { _numRows = numRows; _data = data; _nulls = nulls; } - - public Apache.Arrow.IArrowArray? Result = null; - public void Visit(IByteColumnSource cs) { var arrowBuilder = new Apache.Arrow.Int8Array.Builder(); CopyHelper( @@ -130,13 +132,22 @@ public void Visit(IStringColumnSource cs) { Result = arrowBuilder.Build(); } + public void Visit(IListColumnSource cs) { + var type = cs is IHasElementType ihet ? ihet.ElementType : + throw new Exception($"Expected {Utility.FriendlyTypeName(cs.GetType())} to implement both {nameof(IListColumnSource)} and {nameof(IHasElementType)}"); + + var cb = ColumnBuilder.ForIListWithUnderlyingType(type); + cb.AppendChunk(_data, _nulls); + Result = cb.Build(); + } + private void CopyHelper(TBuilder arrowBuilder) where TArray : Apache.Arrow.IArrowArray where TBuilder : Apache.Arrow.IArrowArrayBuilder { var typedData = ((Chunk)_data).Data; for (var i = 0; i != _numRows; ++i) { if (!_nulls.Data[i]) { - arrowBuilder.Append(typedData[i]); + arrowBuilder.Append(typedData[i]!); } else { arrowBuilder.AppendNull(); } @@ -146,8 +157,7 @@ private void CopyHelper(TBuilder arrowBuilder) } public void Visit(IColumnSource cs) { - throw new NotImplementedException($"No ColumnSourceToArrayVisitor.Visit for {Utility.FriendlyTypeName(cs.GetType())}"); + throw new NotImplementedException($"No {nameof(ColumnSourceToArrowArrayVisitor)}.Visit for {Utility.FriendlyTypeName(cs.GetType())}"); } - } } diff --git a/csharp/client/Dh_NetClient/arrow_util/ArrowColumnSource.cs b/csharp/client/Dh_NetClient/arrow_util/ArrowColumnSource.cs index a27d5872b89..6fa68f5cf65 100644 --- a/csharp/client/Dh_NetClient/arrow_util/ArrowColumnSource.cs +++ b/csharp/client/Dh_NetClient/arrow_util/ArrowColumnSource.cs @@ -13,7 +13,6 @@ global using DateTimeOffsetArrowColumnSource = Deephaven.Dh_NetClient.ArrowColumnSource; global using LocalDateArrowColumnSource = Deephaven.Dh_NetClient.ArrowColumnSource; global using LocalTimeArrowColumnSource = Deephaven.Dh_NetClient.ArrowColumnSource; - using Apache.Arrow; using Apache.Arrow.Types; @@ -181,7 +180,7 @@ public void FillChunk(RowSequence rows, ChunkedArray srcArray) { } sealed class ValueCopier(Chunk typedDest, BooleanChunk? nullFlags, T? deephavenNullValue) - : FillChunkHelper where T : struct { + : FillChunkHelper where T : struct { protected override void DoCopy(IArrowArray src, int srcOffset, int destOffset, int count) { var typedSrc = (IReadOnlyList)src; for (var i = 0; i < count; ++i) { @@ -209,8 +208,12 @@ protected override void DoCopy(IArrowArray src, int srcOffset, int destOffset, i } } -sealed class TransformingCopier(Chunk typedDest, BooleanChunk? nullFlags, - TSrc deephavenNullValue, TDest transformedNullValue, Func transformer) +sealed class TransformingCopier( + Chunk typedDest, + BooleanChunk? nullFlags, + TSrc deephavenNullValue, + TDest transformedNullValue, + Func transformer) : FillChunkHelper where TSrc : struct where TDest : struct { protected override void DoCopy(IArrowArray src, int srcOffset, int destOffset, int count) { var typedSrc = (IReadOnlyList)src; @@ -250,6 +253,31 @@ protected override void DoCopy(IArrowArray src, int srcOffset, int destOffset, i } } +sealed class ListCopier(ListChunk typedDest, BooleanChunk? nullFlags) : FillChunkHelper { + protected override void DoCopy(IArrowArray src, int srcOffset, int destOffset, int count) { + var typedSrc = (ListArray)src; + // var srcValues = (Apache.Arrow.Array)typedSrc.Values; + for (var i = 0; i < count; ++i, ++srcOffset, ++destOffset) { + if (src.IsNull(i)) { + if (nullFlags != null) { + typedDest.Data[destOffset] = null; + nullFlags.Data[destOffset] = true; + } + continue; + } + + var slicedData = typedSrc.GetSlicedValues(srcOffset); + + // var start = typedSrc.ValueOffsets[srcOffset]; + // var end = typedSrc.ValueOffsets[srcOffset + 1]; + // var slicedData = srcValues.Slice(start, end - start); + var sn = new AdaptorSelector(); + slicedData.Accept(sn); + typedDest.Data[destOffset] = sn.Result; + } + } +} + public class ChunkedArrayIterator(ChunkedArray chunkedArray) { private int _arrayIndex = -1; private Int64 _segmentOffset = 0; @@ -299,7 +327,8 @@ class ArrowColumnSourceMaker(ChunkedArray chunkedArray) : IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, - IArrowTypeVisitor { + IArrowTypeVisitor, + IArrowTypeVisitor { public ArrowColumnSource? Result { get; private set; } public void Visit(UInt16Type type) { @@ -350,7 +379,86 @@ public void Visit(Time64Type type) { Result = new LocalTimeArrowColumnSource(chunkedArray); } + public void Visit(ListType type) { + var visitor = new ElementTypeVisitor(); + type.ValueDataType.Accept(visitor); + var elementType = visitor.Result!; + Result = new ListArrowColumnSource(chunkedArray, elementType); + } + public void Visit(IArrowType type) { throw new Exception($"Arrow type {type.Name} is not supported"); } } + +public class ElementTypeVisitor : + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor, + IArrowTypeVisitor { + + public Type? Result { get; private set; } + + public void Visit(UInt16Type type) { + Result = typeof(char); + } + public void Visit(Int8Type type) { + Result = typeof(SByte); + } + public void Visit(Int16Type type) { + Result = typeof(Int16); + } + public void Visit(Int32Type type) { + Result = typeof(Int32); + } + public void Visit(Int64Type type) { + Result = typeof(Int64); + } + public void Visit(FloatType type) { + Result = typeof(float); + } + public void Visit(DoubleType type) { + Result = typeof(double); + } + public void Visit(BooleanType type) { + Result = typeof(bool); + } + public void Visit(StringType type) { + Result = typeof(string); + } + public void Visit(TimestampType type) { + Result = typeof(DateTimeOffset); + } + public void Visit(Date64Type type) { + Result = typeof(DateOnly); + } + public void Visit(Time64Type type) { + Result = typeof(TimeOnly); + } + + public void Visit(IArrowType type) { + throw new Exception($"Arrow type {type.Name} is not supported"); + } +} + +public class ListArrowColumnSource(ChunkedArray chunkedArray, Type elementType) : ArrowColumnSource, IListColumnSource, IHasElementType { + public Type ElementType => elementType; + + public override void FillChunk(RowSequence rows, Chunk dest, BooleanChunk? nullFlags) { + var typedDest = (ListChunk)dest; + var lc = new ListCopier(typedDest, nullFlags); + lc.FillChunk(rows, chunkedArray); + } + + public override void Accept(IColumnSourceVisitor visitor) { + IColumnSource.Accept(this, visitor); + } +} diff --git a/csharp/client/Dh_NetClient/arrow_util/ArrowUtil.cs b/csharp/client/Dh_NetClient/arrow_util/ArrowUtil.cs index 4287b146c8b..5fbbc60c312 100644 --- a/csharp/client/Dh_NetClient/arrow_util/ArrowUtil.cs +++ b/csharp/client/Dh_NetClient/arrow_util/ArrowUtil.cs @@ -189,11 +189,15 @@ public void Visit(ListArray array) { private IEnumerable VisitListArrayHelper(ListArray array) { // ListArray's elements are IArrowArrays. For each element, // we turn the IArrowArray into a List. - // To do this, we recursivel invoke the ToEnumerableVisitor (to handle the case + // To do this, we recursively invoke the ToEnumerableVisitor (to handle the case // where the elements are themselves lists). var innerVisitor = new ToEnumerableVisitor(); for (var i = 0; i != array.Length; ++i) { var slice = array.GetSlicedValues(i); + if (slice == null) { + yield return null; + continue; + } slice.Accept(innerVisitor); yield return new List(innerVisitor.Result.Cast()); } diff --git a/csharp/client/Dh_NetClient/chunk/Chunk.cs b/csharp/client/Dh_NetClient/chunk/Chunk.cs index 39a7c8f27da..10f7c599125 100644 --- a/csharp/client/Dh_NetClient/chunk/Chunk.cs +++ b/csharp/client/Dh_NetClient/chunk/Chunk.cs @@ -13,6 +13,7 @@ global using DateTimeOffsetChunk = Deephaven.Dh_NetClient.Chunk; global using DateOnlyChunk = Deephaven.Dh_NetClient.Chunk; global using TimeOnlyChunk = Deephaven.Dh_NetClient.Chunk; +global using ListChunk = Deephaven.Dh_NetClient.Chunk; namespace Deephaven.Dh_NetClient; @@ -22,12 +23,12 @@ public abstract class Chunk(int size) { public sealed class Chunk : Chunk { public static Chunk Create(int size) { - return new Chunk(new T[size]); + return new Chunk(new T?[size]); } - public T[] Data { get; } + public T?[] Data { get; } - private Chunk(T[] data) : base(data.Length) { + private Chunk(T?[] data) : base(data.Length) { Data = data; } } diff --git a/csharp/client/Dh_NetClient/chunk/ChunkMaker.cs b/csharp/client/Dh_NetClient/chunk/ChunkMaker.cs index bf4c11e272d..d968797b258 100644 --- a/csharp/client/Dh_NetClient/chunk/ChunkMaker.cs +++ b/csharp/client/Dh_NetClient/chunk/ChunkMaker.cs @@ -22,7 +22,8 @@ private class ChunkMakerVisitor(int chunkSize) : IColumnSourceVisitor, IColumnSourceVisitor, IColumnSourceVisitor, - IColumnSourceVisitor { + IColumnSourceVisitor, + IColumnSourceVisitor { public Chunk? Result { get; private set; } public void Visit(ICharColumnSource cs) => Make(cs); @@ -37,6 +38,7 @@ private class ChunkMakerVisitor(int chunkSize) : public void Visit(IDateTimeOffsetColumnSource cs) => Make(cs); public void Visit(IDateOnlyColumnSource cs) => Make(cs); public void Visit(ITimeOnlyColumnSource cs) => Make(cs); + public void Visit(IListColumnSource cs) => Make(cs); public void Visit(IColumnSource cs) { throw new Exception($"Assertion failed: No visitor for type {Utility.FriendlyTypeName(cs.GetType())}"); diff --git a/csharp/client/Dh_NetClient/column/ArrayColumnSource.cs b/csharp/client/Dh_NetClient/column/ArrayColumnSource.cs index 57173564d28..bd5719fbb57 100644 --- a/csharp/client/Dh_NetClient/column/ArrayColumnSource.cs +++ b/csharp/client/Dh_NetClient/column/ArrayColumnSource.cs @@ -104,7 +104,7 @@ public void Visit(IArrowType type) { } public sealed class ArrayColumnSource(int size) : ArrayColumnSource(size), IMutableColumnSource { - private readonly T[] _data = new T[size]; + private readonly T?[] _data = new T?[size]; public override void FillChunk(RowSequence rows, Chunk dest, BooleanChunk? nullFlags) { var typedChunk = (Chunk)dest; diff --git a/csharp/client/Dh_NetClient/column/ColumnSource.cs b/csharp/client/Dh_NetClient/column/ColumnSource.cs index f7373b26919..50987e45560 100644 --- a/csharp/client/Dh_NetClient/column/ColumnSource.cs +++ b/csharp/client/Dh_NetClient/column/ColumnSource.cs @@ -13,6 +13,7 @@ global using IDateTimeOffsetColumnSource = Deephaven.Dh_NetClient.IColumnSource; global using IDateOnlyColumnSource = Deephaven.Dh_NetClient.IColumnSource; global using ITimeOnlyColumnSource = Deephaven.Dh_NetClient.IColumnSource; +global using IListColumnSource = Deephaven.Dh_NetClient.IColumnSource; namespace Deephaven.Dh_NetClient; @@ -59,8 +60,13 @@ public interface IMutableColumnSource : IColumnSource { void FillFromChunk(RowSequence rows, Chunk src, BooleanChunk? nullFlags); } -public interface IMutableColumnSource : IMutableColumnSource, IColumnSource { +public interface IMutableColumnSource : IMutableColumnSource, IColumnSource; +/// +/// Used with interfaces like IColumnSource<IList> that have to expose their element type. +/// +public interface IHasElementType { + Type ElementType { get; } } /// diff --git a/csharp/client/Dh_NetClient/util/ColumnBuilder.cs b/csharp/client/Dh_NetClient/util/ColumnBuilder.cs new file mode 100644 index 00000000000..6c2d3bcb630 --- /dev/null +++ b/csharp/client/Dh_NetClient/util/ColumnBuilder.cs @@ -0,0 +1,441 @@ +// +// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending +// +using Apache.Arrow; +using Apache.Arrow.Types; +using System.Diagnostics.CodeAnalysis; + +namespace Deephaven.Dh_NetClient; + +public abstract class ColumnBuilder { + public static ColumnBuilder ForType(IArrowArrayBuilder? callerProvidedBuilder) { + return (ColumnBuilder)ForType(typeof(T), callerProvidedBuilder); + } + + public static ColumnBuilder ForType(Type type, IArrowArrayBuilder? callerProvidedBuilder) { + var nullableUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullableUnderlyingType != null) { + var miGeneric = typeof(ColumnBuilder).GetMethod(nameof(ForNullableType)) ?? + throw new Exception($"Can't find {nameof(ForNullableType)}"); + var miInstantiated = miGeneric.MakeGenericMethod(nullableUnderlyingType); + return (ColumnBuilder)miInstantiated.Invoke(null, [callerProvidedBuilder])!; + } + + if (type == typeof(sbyte)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder + ?? new Apache.Arrow.Int8Array.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.Int8Type.Default, DeephavenMetadataConstants.Types.Int8, + DeephavenConstants.NullByte); + } + + if (type == typeof(Int16)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.Int16Array.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.Int16Type.Default, DeephavenMetadataConstants.Types.Int16, + DeephavenConstants.NullShort); + } + + if (type == typeof(Int32)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.Int32Array.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.Int32Type.Default, DeephavenMetadataConstants.Types.Int32, + DeephavenConstants.NullInt); + } + + if (type == typeof(Int64)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.Int64Array.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.Int64Type.Default, DeephavenMetadataConstants.Types.Int64, + DeephavenConstants.NullLong); + } + + if (type == typeof(float)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.FloatArray.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.FloatType.Default, DeephavenMetadataConstants.Types.Float, + DeephavenConstants.NullFloat); + } + + if (type == typeof(double)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.DoubleArray.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.DoubleType.Default, DeephavenMetadataConstants.Types.Double, + DeephavenConstants.NullDouble); + } + + if (type == typeof(bool)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.BooleanArray.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.BooleanType.Default, DeephavenMetadataConstants.Types.Bool, + null); + } + + if (type == typeof(char)) { + var builderToUse = + (Apache.Arrow.UInt16Array.Builder?)callerProvidedBuilder ?? + new Apache.Arrow.UInt16Array.Builder(); + return new CharColumnBuilder(builderToUse); + } + + if (type == typeof(string)) { + var builderToUse = + (Apache.Arrow.StringArray.Builder?)callerProvidedBuilder ?? + new Apache.Arrow.StringArray.Builder(); + return new StringColumnBuilder(builderToUse); + } + + if (type == typeof(DateTimeOffset)) { + var dataType = new Apache.Arrow.Types.TimestampType(TimeUnit.Nanosecond, "UTC"); + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.TimestampArray.Builder(dataType); + return new TypicalBuilder( + builderToUse, dataType, DeephavenMetadataConstants.Types.DateTime, null); + } + + if (type == typeof(DateOnly)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.Date64Array.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.Date64Type.Default, DeephavenMetadataConstants.Types.LocalDate, + null); + } + + if (type == typeof(TimeOnly)) { + var builderToUse = + (IArrowArrayBuilder?)callerProvidedBuilder ?? + new Apache.Arrow.Time64Array.Builder(); + return new TypicalBuilder( + builderToUse, Apache.Arrow.Types.Time64Type.Default, DeephavenMetadataConstants.Types.LocalTime, + null); + } + + if (TryMatchTypeToIListOfUnderlying(type, out var underlyingType)) { + var miGeneric = typeof(ColumnBuilder).GetMethod(nameof(ForIListType)) ?? + throw new Exception($"Can't find {nameof(ForIListType)}"); + var miInstantiated = miGeneric.MakeGenericMethod(type, underlyingType); + return (ColumnBuilder)miInstantiated.Invoke(null, [callerProvidedBuilder])!; + } + + throw new Exception($"ColumnBuilder does not support type {Utility.FriendlyTypeName(type)}"); + } + + public static ColumnBuilder ForNullableType(IArrowArrayBuilder? callerProvidedBuilder) where T : struct { + var underlyingCb = ForType(callerProvidedBuilder); + return new NullableBuilder(underlyingCb); + } + + public static ColumnBuilder ForIListWithUnderlyingType(Type underlyingType) { + var underlyingTypeToUse = underlyingType.IsValueType ? typeof(Nullable<>).MakeGenericType(underlyingType) : underlyingType; + var ilistType = typeof(IList<>).MakeGenericType(underlyingTypeToUse); + var miGeneric = typeof(ColumnBuilder).GetMethod(nameof(ForIListType)) ?? + throw new Exception($"Can't find {nameof(ForIListType)}"); + var miInstantiated = miGeneric.MakeGenericMethod(ilistType, underlyingTypeToUse); + return (ColumnBuilder)miInstantiated.Invoke(null, [null])!; + } + + public static ColumnBuilder ForIListType( + IArrowArrayBuilder? callerProvidedBuilder) where TList : class, IList { + Apache.Arrow.ListArray.Builder builderToUse; + if (callerProvidedBuilder == null) { + // Make a temporary column builder just so I can get the correct Arrow data type + var tempCb = ForType(null); + var (underlyingArrowType, _, _) = tempCb.GetTypeInfo(); + builderToUse = new Apache.Arrow.ListArray.Builder(underlyingArrowType); + } else { + builderToUse = (Apache.Arrow.ListArray.Builder)callerProvidedBuilder; + } + return new ListBuilder(builderToUse); + } + + /// + /// Is target an IList<T> or does it inherit from IList<T> for some T? + /// If so, set underlying to T and return true. Otherwise return false. + /// + /// Type to examine + /// The underlying type if target is an IList<T> + /// True if target is an IList<T> or inherits from IList<T>, otherwise false + + private static bool TryMatchTypeToIListOfUnderlying(Type target, + [MaybeNullWhen(false)] out Type underlying) { + if (TryMatch(target, out underlying)) { + return true; + } + foreach (var iface in target.GetInterfaces()) { + if (TryMatch(iface, out underlying)) { + return true; + } + } + underlying = null; + return false; + + // Is target an IList for some T? If so, set underlying to T and return true. Otherwise return false. + static bool TryMatch(Type target, [MaybeNullWhen(false)] out Type underlying) { + if (target.IsGenericType && target.GetGenericTypeDefinition() == typeof(IList<>)) { + underlying = target.GetGenericArguments()[0]; + return true; + } + underlying = null; + return false; + } + } + + public abstract void AppendChunk(Chunk data, BooleanChunk nulls); + public abstract Apache.Arrow.IArrowArray Build(); + public abstract (Apache.Arrow.Types.IArrowType, string, string?) GetTypeInfo(); + public abstract void AppendNull(); +} + +public abstract class ColumnBuilder : ColumnBuilder { + public abstract void Append(T item); +} + +public sealed class TypicalBuilder : ColumnBuilder + where T : struct, IEquatable + where TArray : Apache.Arrow.IArrowArray + where TBuilder : Apache.Arrow.IArrowArrayBuilder { + private readonly Apache.Arrow.IArrowArrayBuilder _builder; + private readonly Apache.Arrow.Types.IArrowType _arrowType; + private readonly string _deephavenTypeName; + private readonly T? _deephavenNullValue; + + public TypicalBuilder(Apache.Arrow.IArrowArrayBuilder builder, + Apache.Arrow.Types.IArrowType arrowType, string deephavenTypeName, T? deephavenNullValue) { + _builder = builder; + _arrowType = arrowType; + _deephavenTypeName = deephavenTypeName; + _deephavenNullValue = deephavenNullValue; + } + + public override void Append(T item) { + if (_deephavenNullValue.HasValue && _deephavenNullValue.Value.Equals(item)) { + _builder.AppendNull(); + } else { + _builder.Append(item); + } + } + + public override void AppendNull() { + _builder.AppendNull(); + } + + public override void AppendChunk(Chunk data, BooleanChunk nulls) { + if (data.Size != nulls.Size) { + throw new ArgumentException($"Chunk size {data.Size} does not match nulls size {nulls.Size}"); + } + var typedChunk = data as Chunk + ?? throw new ArgumentException($"Expected chunk of type {Utility.FriendlyTypeName(typeof(Chunk))}, but got {Utility.FriendlyTypeName(data.GetType())}"); + + for (var i = 0; i != typedChunk.Size; i++) { + if (nulls.Data[i]) { + AppendNull(); + } else { + Append(typedChunk.Data[i]); + } + } + } + + public override (IArrowType, string, string?) GetTypeInfo() { + return (_arrowType, _deephavenTypeName, null); + } + + public override Apache.Arrow.IArrowArray Build() { + return _builder.Build(null); + } +} + +public sealed class CharColumnBuilder : ColumnBuilder { + private readonly Apache.Arrow.UInt16Array.Builder _builder; + + public CharColumnBuilder(UInt16Array.Builder builder) { + _builder = builder; + } + + public override void Append(char item) { + if (item == DeephavenConstants.NullChar) { + _builder.AppendNull(); + } else { + _builder.Append(item); + } + } + + public override void AppendNull() { + _builder.AppendNull(); + } + + public override void AppendChunk(Chunk data, BooleanChunk nulls) { + if (data.Size != nulls.Size) { + throw new ArgumentException($"Chunk size {data.Size} does not match nulls size {nulls.Size}"); + } + var typedChunk = data as CharChunk + ?? throw new ArgumentException($"Expected chunk of type {Utility.FriendlyTypeName(typeof(CharChunk))}, but got {Utility.FriendlyTypeName(data.GetType())}"); + + for (var i = 0; i != typedChunk.Size; i++) { + if (nulls.Data[i]) { + AppendNull(); + } else { + Append(typedChunk.Data[i]); + } + } + } + + public override (IArrowType, string, string?) GetTypeInfo() { + return (Apache.Arrow.Types.UInt16Type.Default, DeephavenMetadataConstants.Types.Char16, null); + } + + public override Apache.Arrow.IArrowArray Build() { + return _builder.Build(); + } +} + +public sealed class StringColumnBuilder : ColumnBuilder { + private readonly Apache.Arrow.StringArray.Builder _builder; + + public StringColumnBuilder(StringArray.Builder builder) { + _builder = builder; + } + + public override void Append(string item) { + _builder.Append(item); + } + + public override void AppendNull() { + _builder.AppendNull(); + } + + public override void AppendChunk(Chunk data, BooleanChunk nulls) { + if (data.Size != nulls.Size) { + throw new ArgumentException($"Chunk size {data.Size} does not match nulls size {nulls.Size}"); + } + var typedChunk = data as StringChunk + ?? throw new ArgumentException($"Expected chunk of type {Utility.FriendlyTypeName(typeof(StringChunk))}, but got {Utility.FriendlyTypeName(data.GetType())}"); + + for (var i = 0; i != typedChunk.Size; i++) { + if (nulls.Data[i]) { + AppendNull(); + } else { + Append(typedChunk.Data[i]!); + } + } + } + + + public override (IArrowType, string, string?) GetTypeInfo() { + return (Apache.Arrow.Types.StringType.Default, DeephavenMetadataConstants.Types.String, null); + } + + public override Apache.Arrow.IArrowArray Build() { + return _builder.Build(); + } +} + +public sealed class NullableBuilder : ColumnBuilder where T : struct { + private readonly ColumnBuilder _underlyingBuilder; + + public NullableBuilder(ColumnBuilder underlyingBuilder) { + _underlyingBuilder = underlyingBuilder; + } + + public override void Append(T? item) { + if (item.HasValue) { + _underlyingBuilder.Append(item.Value); + } else { + _underlyingBuilder.AppendNull(); + } + } + + public override void AppendNull() { + _underlyingBuilder.AppendNull(); + } + + public override void AppendChunk(Chunk data, BooleanChunk nulls) { + if (data.Size != nulls.Size) { + throw new ArgumentException($"Chunk size {data.Size} does not match nulls size {nulls.Size}"); + } + var typedChunk = data as Chunk + ?? throw new ArgumentException($"Expected chunk of type {Utility.FriendlyTypeName(typeof(Chunk))}, but got {Utility.FriendlyTypeName(data.GetType())}"); + + for (var i = 0; i != typedChunk.Size; i++) { + if (nulls.Data[i]) { + AppendNull(); + } else { + Append(typedChunk.Data[i]); + } + } + } + + public override Apache.Arrow.IArrowArray Build() { + return _underlyingBuilder.Build(); + } + + public override (IArrowType, string, string?) GetTypeInfo() { + return _underlyingBuilder.GetTypeInfo(); + } +} + +public sealed class ListBuilder : ColumnBuilder where TList : class, IList { + private readonly Apache.Arrow.ListArray.Builder _listBuilder; + private readonly ColumnBuilder _underlyingBuilder; + + public ListBuilder(Apache.Arrow.ListArray.Builder listBuilder) { + _listBuilder = listBuilder; + _underlyingBuilder = ColumnBuilder.ForType(_listBuilder.ValueBuilder); + } + + public override void Append(TList list) { + _listBuilder.Append(); + foreach (var element in list) { + _underlyingBuilder.Append(element); + } + } + + public override void AppendNull() { + _listBuilder.AppendNull(); + } + + public override void AppendChunk(Chunk data, BooleanChunk nulls) { + if (data.Size != nulls.Size) { + throw new ArgumentException($"Chunk size {data.Size} does not match nulls size {nulls.Size}"); + } + var typedChunk = data as ListChunk + ?? throw new ArgumentException($"Expected chunk of type {typeof(ListChunk)}, but got {data.GetType()}"); + + for (var i = 0; i != typedChunk.Size; i++) { + if (nulls.Data[i]) { + AppendNull(); + } else { + var typedElement = typedChunk.Data[i] as TList ?? + throw new ArgumentException($"Expected element {i} to be of type {typeof(TList)}, but got {typedChunk.Data[i]?.GetType()}"); + Append(typedElement); + } + } + } + + public override IArrowArray Build() { + return _listBuilder.Build(); + } + + public override (IArrowType, string, string?) GetTypeInfo() { + var (underlyingArrowType, underlyingDeephavenType, _) = _underlyingBuilder.GetTypeInfo(); + + var arrowType = new Apache.Arrow.Types.ListType(underlyingArrowType); + var deephavenType = underlyingDeephavenType + "[]"; + var componentType = underlyingDeephavenType; + return (arrowType, deephavenType, componentType); + } +} diff --git a/csharp/client/Dh_NetClient/util/TableComparer.cs b/csharp/client/Dh_NetClient/util/TableComparer.cs index 88b87665167..caf52d34dd2 100644 --- a/csharp/client/Dh_NetClient/util/TableComparer.cs +++ b/csharp/client/Dh_NetClient/util/TableComparer.cs @@ -2,8 +2,6 @@ // Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending // using System.Collections; -using System.Diagnostics; -using Apache.Arrow; namespace Deephaven.Dh_NetClient; diff --git a/csharp/client/Dh_NetClient/util/TableMaker.cs b/csharp/client/Dh_NetClient/util/TableMaker.cs index 87203d0479d..032a78328eb 100644 --- a/csharp/client/Dh_NetClient/util/TableMaker.cs +++ b/csharp/client/Dh_NetClient/util/TableMaker.cs @@ -1,9 +1,6 @@ // // Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending // -using Apache.Arrow; -using Apache.Arrow.Types; - namespace Deephaven.Dh_NetClient; public class TableMaker { @@ -12,6 +9,10 @@ public class TableMaker { public void AddColumn(string name, IReadOnlyList values) { var cb = ColumnBuilder.ForType(null); foreach (var value in values) { + if (value == null) { + cb.AppendNull(); + continue; + } cb.Append(value); } var array = cb.Build(); @@ -121,332 +122,6 @@ public override string ToString() { return ToString(true); } - private class ColumnBuilder { - public static ColumnBuilder ForType(IArrowArrayBuilder? callerProvidedBuilder) { - return (ColumnBuilder)ForType(typeof(T), callerProvidedBuilder); - } - - public static ColumnBuilder ForType(Type type, IArrowArrayBuilder? callerProvidedBuilder) { - var nullableUnderlyingType = Nullable.GetUnderlyingType(type); - if (nullableUnderlyingType != null) { - var miGeneric = typeof(ColumnBuilder).GetMethod(nameof(ForNullableType)) ?? - throw new Exception($"Can't find {nameof(ForNullableType)}"); - var miInstantiated = miGeneric.MakeGenericMethod(nullableUnderlyingType); - return (ColumnBuilder)miInstantiated.Invoke(null, [callerProvidedBuilder])!; - } - - if (type == typeof(sbyte)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder - ?? new Apache.Arrow.Int8Array.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.Int8Type.Default, DeephavenMetadataConstants.Types.Int8, - DeephavenConstants.NullByte); - } - - if (type == typeof(Int16)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.Int16Array.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.Int16Type.Default, DeephavenMetadataConstants.Types.Int16, - DeephavenConstants.NullShort); - } - - if (type == typeof(Int32)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.Int32Array.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.Int32Type.Default, DeephavenMetadataConstants.Types.Int32, - DeephavenConstants.NullInt); - } - - if (type == typeof(Int64)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.Int64Array.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.Int64Type.Default, DeephavenMetadataConstants.Types.Int64, - DeephavenConstants.NullLong); - } - - if (type == typeof(float)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.FloatArray.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.FloatType.Default, DeephavenMetadataConstants.Types.Float, - DeephavenConstants.NullFloat); - } - - if (type == typeof(double)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.DoubleArray.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.DoubleType.Default, DeephavenMetadataConstants.Types.Double, - DeephavenConstants.NullDouble); - } - - if (type == typeof(bool)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.BooleanArray.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.BooleanType.Default, DeephavenMetadataConstants.Types.Bool, - null); - } - - if (type == typeof(char)) { - var builderToUse = - (Apache.Arrow.UInt16Array.Builder?)callerProvidedBuilder ?? - new Apache.Arrow.UInt16Array.Builder(); - return new CharColumnBuilder(builderToUse); - } - - if (type == typeof(string)) { - var builderToUse = - (Apache.Arrow.StringArray.Builder?)callerProvidedBuilder ?? - new Apache.Arrow.StringArray.Builder(); - return new StringColumnBuilder(builderToUse); - } - - if (type == typeof(DateTimeOffset)) { - var dataType = new Apache.Arrow.Types.TimestampType(TimeUnit.Nanosecond, "UTC"); - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.TimestampArray.Builder(dataType); - return new TypicalBuilder( - builderToUse, dataType, DeephavenMetadataConstants.Types.DateTime, null); - } - - if (type == typeof(DateOnly)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.Date64Array.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.Date64Type.Default, DeephavenMetadataConstants.Types.LocalDate, - null); - } - - if (type == typeof(TimeOnly)) { - var builderToUse = - (IArrowArrayBuilder?)callerProvidedBuilder ?? - new Apache.Arrow.Time64Array.Builder(); - return new TypicalBuilder( - builderToUse, Apache.Arrow.Types.Time64Type.Default, DeephavenMetadataConstants.Types.LocalTime, - null); - } - - var listUnderlyingType = GetIListInterfaceUnderlyingType(type); - if (listUnderlyingType != null) { - var miGeneric = typeof(ColumnBuilder).GetMethod(nameof(ForIListType)) ?? - throw new Exception($"Can't find {nameof(ForIListType)}"); - var miInstantiated = miGeneric.MakeGenericMethod(type, listUnderlyingType); - return (ColumnBuilder)miInstantiated.Invoke(null, [callerProvidedBuilder])!; - } - - throw new Exception($"ColumnBuilder does not support type {Utility.FriendlyTypeName(type)}"); - } - - public static ColumnBuilder ForNullableType(IArrowArrayBuilder? callerProvidedBuilder) where T : struct { - var underlyingCb = ForType(callerProvidedBuilder); - return new NullableBuilder(underlyingCb); - } - - public static ColumnBuilder ForIListType( - IArrowArrayBuilder? callerProvidedBuilder) where TList : IList { - Apache.Arrow.ListArray.Builder builderToUse; - if (callerProvidedBuilder == null) { - // Make a temporary column builder just so I can get the correct Arrow data type - var tempCb = ForType(null); - var (underlyingArrowType, _, _) = tempCb.GetTypeInfo(); - builderToUse = new Apache.Arrow.ListArray.Builder(underlyingArrowType); - } else { - builderToUse = (Apache.Arrow.ListArray.Builder)callerProvidedBuilder; - } - return new ListBuilder(builderToUse); - } - - private static Type? GetIListInterfaceUnderlyingType(Type ilistType) { - var temp = ilistType.GetInterfaces(); - return ilistType.GetInterfaces().Select(GetIListUnderlyingType).FirstOrDefault(t => t != null); - } - - private static Type? GetIListUnderlyingType(Type ilistType) { - if (ilistType.IsGenericType && !ilistType.IsGenericTypeDefinition) { - // Instantiated generic type only - var genericType = ilistType.GetGenericTypeDefinition(); - if (ReferenceEquals(genericType, typeof(IList<>))) { - return ilistType.GetGenericArguments()[0]; - } - } - return null; - } - } - - private abstract class ColumnBuilder : ColumnBuilder { - public abstract void Append(T item); - public abstract void AppendNull(); - - public abstract Apache.Arrow.IArrowArray Build(); - - public abstract (Apache.Arrow.Types.IArrowType, string, string?) GetTypeInfo(); - } - - private sealed class TypicalBuilder : ColumnBuilder - where T : struct, IEquatable - where TArray : Apache.Arrow.IArrowArray - where TBuilder : Apache.Arrow.IArrowArrayBuilder { - private readonly Apache.Arrow.IArrowArrayBuilder _builder; - private readonly Apache.Arrow.Types.IArrowType _arrowType; - private readonly string _deephavenTypeName; - private readonly T? _deephavenNullValue; - - public TypicalBuilder(Apache.Arrow.IArrowArrayBuilder builder, - Apache.Arrow.Types.IArrowType arrowType, string deephavenTypeName, T? deephavenNullValue) { - _builder = builder; - _arrowType = arrowType; - _deephavenTypeName = deephavenTypeName; - _deephavenNullValue = deephavenNullValue; - } - - public override void Append(T item) { - if (_deephavenNullValue.HasValue && _deephavenNullValue.Value.Equals(item)) { - _builder.AppendNull(); - } else { - _builder.Append(item); - } - } - - public override void AppendNull() { - _builder.AppendNull(); - } - - public override (IArrowType, string, string?) GetTypeInfo() { - return (_arrowType, _deephavenTypeName, null); - } - - public override Apache.Arrow.IArrowArray Build() { - return _builder.Build(null); - } - } - - private sealed class CharColumnBuilder : ColumnBuilder { - private readonly Apache.Arrow.UInt16Array.Builder _builder; - - public CharColumnBuilder(UInt16Array.Builder builder) { - _builder = builder; - } - - public override void Append(char item) { - if (item == DeephavenConstants.NullChar) { - _builder.AppendNull(); - } else { - _builder.Append(item); - } - } - - public override void AppendNull() { - _builder.AppendNull(); - } - - public override (IArrowType, string, string?) GetTypeInfo() { - return (Apache.Arrow.Types.UInt16Type.Default, DeephavenMetadataConstants.Types.Char16, null); - } - - public override Apache.Arrow.IArrowArray Build() { - return _builder.Build(); - } - } - - private sealed class StringColumnBuilder : ColumnBuilder { - private readonly Apache.Arrow.StringArray.Builder _builder; - - public StringColumnBuilder(StringArray.Builder builder) { - _builder = builder; - } - - public override void Append(string item) { - _builder.Append(item); - } - - public override void AppendNull() { - _builder.AppendNull(); - } - - public override (IArrowType, string, string?) GetTypeInfo() { - return (Apache.Arrow.Types.StringType.Default, DeephavenMetadataConstants.Types.String, null); - } - - public override Apache.Arrow.IArrowArray Build() { - return _builder.Build(); - } - } - - private sealed class NullableBuilder : ColumnBuilder where T : struct { - private readonly ColumnBuilder _underlyingBuilder; - - public NullableBuilder(ColumnBuilder underlyingBuilder) { - _underlyingBuilder = underlyingBuilder; - } - - public override void Append(T? item) { - if (item.HasValue) { - _underlyingBuilder.Append(item.Value); - } else { - _underlyingBuilder.AppendNull(); - } - } - - public override void AppendNull() { - _underlyingBuilder.AppendNull(); - } - - public override Apache.Arrow.IArrowArray Build() { - return _underlyingBuilder.Build(); - } - - public override (IArrowType, string, string?) GetTypeInfo() { - return _underlyingBuilder.GetTypeInfo(); - } - } - - private class ListBuilder : ColumnBuilder where TList : IList { - private readonly Apache.Arrow.ListArray.Builder _listBuilder; - private readonly ColumnBuilder _underlyingBuilder; - - public ListBuilder(Apache.Arrow.ListArray.Builder listBuilder) { - _listBuilder = listBuilder; - _underlyingBuilder = ColumnBuilder.ForType(_listBuilder.ValueBuilder); - } - - public override void Append(TList list) { - _listBuilder.Append(); - foreach (var element in list) { - _underlyingBuilder.Append(element); - } - } - - public override void AppendNull() { - _listBuilder.AppendNull(); - } - - public override IArrowArray Build() { - return _listBuilder.Build(); - } - - public override (IArrowType, string, string?) GetTypeInfo() { - var (underlyingArrowType, underlyingDeephavenType, _) = _underlyingBuilder.GetTypeInfo(); - - var arrowType = new Apache.Arrow.Types.ListType(underlyingArrowType); - var deephavenType = underlyingDeephavenType + "[]"; - var componentType = underlyingDeephavenType; - return (arrowType, deephavenType, componentType); - } - } - private record ColumnInfo(string Name, Apache.Arrow.IArrowArray Data, KeyValuePair[] ArrowMetadata); diff --git a/csharp/client/Dh_NetClientTests/AddDropTest.cs b/csharp/client/Dh_NetClientTests/AddDropTest.cs index 077d13c966a..978fe9b980f 100644 --- a/csharp/client/Dh_NetClientTests/AddDropTest.cs +++ b/csharp/client/Dh_NetClientTests/AddDropTest.cs @@ -2,25 +2,24 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class AddDropTest(ITestOutputHelper output) { - [Fact] - public void TestDropSomeColumns() { +public class AddDropTest { + [Test] + public async Task TestDropSomeColumns() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; var t = table.Update("II = ii").Where("Ticker == `AAPL`"); var cn = ctx.ColumnNames; var t2 = t.DropColumns(cn.ImportDate, cn.Ticker, cn.Open, cn.Close); - output.WriteLine(t2.ToString(true)); + Console.WriteLine(t2.ToString(true)); var expected = new TableMaker(); expected.AddColumn("Volume", [(Int64)100000, 250000, 19000]); expected.AddColumn("II", [(Int64)5, 6, 7]); - TableComparer.AssertSame(expected, t2); + await Assert.That(() => TableComparer.AssertSame(expected, t2)).ThrowsNothing(); } -} +} \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/AggregatesTest.cs b/csharp/client/Dh_NetClientTests/AggregatesTest.cs index 20bc867c6b4..d38ae55c157 100644 --- a/csharp/client/Dh_NetClientTests/AggregatesTest.cs +++ b/csharp/client/Dh_NetClientTests/AggregatesTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class AggregatesTest { - [Fact] - public void TestVariousAggregates() { + [Test] + public async Task TestVariousAggregates() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; @@ -30,6 +30,6 @@ public void TestVariousAggregates() { expected.AddColumn("MaxClose", [544.9]); expected.AddColumn("Count", [(Int64)2]); - TableComparer.AssertSame(expected, aggTable); + await Assert.That(() => TableComparer.AssertSame(expected, aggTable)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/Dh_NetClientTests.csproj b/csharp/client/Dh_NetClientTests/Dh_NetClientTests.csproj index c99d8441376..a32182a5002 100644 --- a/csharp/client/Dh_NetClientTests/Dh_NetClientTests.csproj +++ b/csharp/client/Dh_NetClientTests/Dh_NetClientTests.csproj @@ -1,7 +1,7 @@  - net10.0 + net8.0;net10.0 enable enable @@ -13,18 +13,12 @@ - - - + - - - - diff --git a/csharp/client/Dh_NetClientTests/FilterTest.cs b/csharp/client/Dh_NetClientTests/FilterTest.cs index d970ee33387..81ed19a6e6e 100644 --- a/csharp/client/Dh_NetClientTests/FilterTest.cs +++ b/csharp/client/Dh_NetClientTests/FilterTest.cs @@ -2,19 +2,18 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class FilterTest(ITestOutputHelper output) { - [Fact] - public void TestFilterATable() { +public class FilterTest { + [Test] + public async Task TestFilterATable() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; var t1 = table.Where( "ImportDate == `2017-11-01` && Ticker == `AAPL` && (Close <= 120.0 || isNull(Close))"); - output.WriteLine(t1.ToString(true)); + Console.WriteLine(t1.ToString(true)); var expected = new TableMaker(); expected.AddColumn("ImportDate", ["2017-11-01", "2017-11-01", "2017-11-01"]); @@ -23,6 +22,6 @@ public void TestFilterATable() { expected.AddColumn("Close", [23.5, 24.2, 26.7]); expected.AddColumn("Volume", [(Int64)100000, 250000, 19000]); - TableComparer.AssertSame(expected, t1); + await Assert.That(() => TableComparer.AssertSame(expected, t1)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/GlobalUsings.cs b/csharp/client/Dh_NetClientTests/GlobalUsings.cs index c7d17c5b1f9..54710763fbb 100644 --- a/csharp/client/Dh_NetClientTests/GlobalUsings.cs +++ b/csharp/client/Dh_NetClientTests/GlobalUsings.cs @@ -1,4 +1,3 @@ // // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // -global using Xunit; \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/GroupTest.cs b/csharp/client/Dh_NetClientTests/GroupTest.cs index 5951b9a8ed5..5bbfc9a2ac9 100644 --- a/csharp/client/Dh_NetClientTests/GroupTest.cs +++ b/csharp/client/Dh_NetClientTests/GroupTest.cs @@ -2,13 +2,12 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class GroupTest(ITestOutputHelper output) { - [Fact] - public void GroupATable() { +public class GroupTest { + [Test] + public async Task GroupATable() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var maker = new TableMaker(); @@ -32,7 +31,7 @@ public void GroupATable() { using var t1 = maker.MakeTable(ctx.Client.Manager); using var grouped = t1.By("Type"); - output.WriteLine(grouped.ToString(true, true)); + Console.WriteLine(grouped.ToString(true, true)); var expected = new TableMaker(); expected.AddColumn("Type", ["Granny Smith", "Gala", "Golden Delicious"]); @@ -40,11 +39,11 @@ public void GroupATable() { [["Green", "Green"], ["Red-Green", "Orange-Green"], ["Yellow", "Yellow"]]); expected.AddColumn>("Weight", [[102, 85], [79, 92], [78, 99]]); expected.AddColumn>("Calories", [[53, 48], [51, 61], [46, 57]]); - TableComparer.AssertSame(expected, grouped); + await Assert.That(() => TableComparer.AssertSame(expected, grouped)).ThrowsNothing(); } - [Fact] - public void NestedListsNotSupported() { + [Test] + public async Task NestedListsNotSupported() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var maker = new TableMaker(); @@ -53,6 +52,6 @@ public void NestedListsNotSupported() { [[4, 5]] ]); using var t = maker.MakeTable(ctx.Client.Manager); - Assert.Throws(t.ToClientTable); + await Assert.That(() => t.ToClientTable()).Throws(); } -} +} \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/HeadAndTailTest.cs b/csharp/client/Dh_NetClientTests/HeadAndTailTest.cs index f619176a13a..cc4fe1cd8c9 100644 --- a/csharp/client/Dh_NetClientTests/HeadAndTailTest.cs +++ b/csharp/client/Dh_NetClientTests/HeadAndTailTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class HeadAndTailTest { - [Fact] - public void TestHeadAndTail() { + [Test] + public async Task TestHeadAndTail() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; @@ -21,7 +21,7 @@ public void TestHeadAndTail() { expected.AddColumn("Ticker", ["XRX", "XRX"]); expected.AddColumn("Volume", [(Int64)345000, 87000]); - TableComparer.AssertSame(expected, th); + await Assert.That(() => TableComparer.AssertSame(expected, th)).ThrowsNothing(); } { @@ -29,7 +29,7 @@ public void TestHeadAndTail() { expected.AddColumn("Ticker", ["ZNGA", "ZNGA"]); expected.AddColumn("Volume", [(Int64)46123, 48300]); - TableComparer.AssertSame(expected, tt); + await Assert.That(() => TableComparer.AssertSame(expected, tt)).ThrowsNothing(); } } } diff --git a/csharp/client/Dh_NetClientTests/InputTableTest.cs b/csharp/client/Dh_NetClientTests/InputTableTest.cs index 0eb2de38df6..e2c67582b1b 100644 --- a/csharp/client/Dh_NetClientTests/InputTableTest.cs +++ b/csharp/client/Dh_NetClientTests/InputTableTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class InputTableTest { - [Fact] - public void TestInputTableAppend() { + [Test] + public async Task TestInputTableAppend() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -20,7 +20,7 @@ public void TestInputTableAppend() { var expected = new TableMaker(); expected.AddColumn("A", [(Int64)0, 1, 2]); expected.AddColumn("B", [(Int64)100, 101, 102]); - TableComparer.AssertSame(expected, inputTable); + await Assert.That(() => TableComparer.AssertSame(expected, inputTable)).ThrowsNothing(); } var tableToAdd = tm.EmptyTable(2).Update("A = ii", "B = ii + 200"); @@ -33,13 +33,13 @@ public void TestInputTableAppend() { var expected = new TableMaker(); expected.AddColumn("A", aData); expected.AddColumn("B", bData); - TableComparer.AssertSame(expected, inputTable); + await Assert.That(() => TableComparer.AssertSame(expected, inputTable)).ThrowsNothing(); } } - [Fact] - public void TestInputTableKeyed() { + [Test] + public async Task TestInputTableKeyed() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -48,28 +48,27 @@ public void TestInputTableKeyed() { var inputTable = tm.InputTable(source, "A"); - // expect input_table to be {0, 100}, {1, 101}, {2, 102} + // expect inputTable to be {0, 100}, {1, 101}, {2, 102} { var aData = new Int64[] { 0, 1, 2 }; var bData = new Int64[] { 100, 101, 102 }; var expected = new TableMaker(); expected.AddColumn("A", aData); expected.AddColumn("B", bData); - TableComparer.AssertSame(expected, inputTable); + await Assert.That(() => TableComparer.AssertSame(expected, inputTable)).ThrowsNothing(); } - var tableToAdd = tm.EmptyTable(2).Update("A = ii", "B = ii + 200"); inputTable.AddTable(tableToAdd); - // Because key is "A", expect input_table to be {0, 200}, {1, 201}, {2, 102} + // Because key is "A", expect inputTable to be {0, 200}, {1, 201}, {2, 102} { var aData = new Int64[] { 0, 1, 2 }; var bData = new Int64[] { 200, 201, 102 }; var expected = new TableMaker(); expected.AddColumn("A", aData); expected.AddColumn("B", bData); - TableComparer.AssertSame(expected, inputTable); + await Assert.That(() => TableComparer.AssertSame(expected, inputTable)).ThrowsNothing(); } } } diff --git a/csharp/client/Dh_NetClientTests/JoinTest.cs b/csharp/client/Dh_NetClientTests/JoinTest.cs index eae5b38c292..3615b15ccea 100644 --- a/csharp/client/Dh_NetClientTests/JoinTest.cs +++ b/csharp/client/Dh_NetClientTests/JoinTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class JoinTest { - [Fact] - public void TestJoin() { + [Test] + public async Task TestJoin() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var testTable = ctx.TestTable; @@ -23,11 +23,11 @@ public void TestJoin() { expected.AddColumn("Close", [53.8, 88.5, 38.7, 453, 26.7, 544.9]); expected.AddColumn("ADV", [216000, 6060842, 138000, 138000000, 123000, 47211.50]); - TableComparer.AssertSame(expected, filtered); + await Assert.That(() => TableComparer.AssertSame(expected, filtered)).ThrowsNothing(); } - [Fact] - public void TestAj() { + [Test] + public async Task TestAj() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -84,12 +84,12 @@ public void TestAj() { expected.AddColumn("BidSize", [(int?)null, 20, 20, 5, 13]); expected.AddColumn("Ask", [(double?)null, 3.4, 3.4, 105, 110]); expected.AddColumn("AskSize", [(int?)null, 33, 33, 47, 15]); - TableComparer.AssertSame(expected, result); + await Assert.That(() => TableComparer.AssertSame(expected, result)).ThrowsNothing(); } } - [Fact] - public void TestRaj() { + [Test] + public async Task TestRaj() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -147,7 +147,7 @@ public void TestRaj() { expected.AddColumn("Ask", [(double?)2.5, null, null, 105, 110]); expected.AddColumn("AskSize", [(int?)83, null, null, 47, 15]); - TableComparer.AssertSame(expected, result); + await Assert.That(() => TableComparer.AssertSame(expected, result)).ThrowsNothing(); } } } diff --git a/csharp/client/Dh_NetClientTests/LastByTest.cs b/csharp/client/Dh_NetClientTests/LastByTest.cs index 3728014d05e..13e19ecccba 100644 --- a/csharp/client/Dh_NetClientTests/LastByTest.cs +++ b/csharp/client/Dh_NetClientTests/LastByTest.cs @@ -2,15 +2,12 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class LastByTest(ITestOutputHelper output) { - private readonly ITestOutputHelper _output = output; - - [Fact] - public void TestLastBy() { +public class LastByTest { + [Test] + public async Task TestLastBy() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var testTable = ctx.TestTable; @@ -29,6 +26,6 @@ public void TestLastBy() { expected.AddColumn("Open", openData); expected.AddColumn("Close", closeData); - TableComparer.AssertSame(expected, lb); + await Assert.That(() => TableComparer.AssertSame(expected, lb)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/MergeTest.cs b/csharp/client/Dh_NetClientTests/MergeTest.cs index 956df0ca8f3..6dc8cb11fdf 100644 --- a/csharp/client/Dh_NetClientTests/MergeTest.cs +++ b/csharp/client/Dh_NetClientTests/MergeTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class MergeTest { - [Fact] - public void TestMerge() { + [Test] + public async Task TestMerge() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var testTable = ctx.TestTable; @@ -29,6 +29,6 @@ public void TestMerge() { expected.AddColumn("Close", [23.5, 24.2, 26.7, 538.2, 544.9]); expected.AddColumn("Volume", [(Int64)100000, 250000, 19000, 46123, 48300]); - TableComparer.AssertSame(expected, merged); + await Assert.That(() => TableComparer.AssertSame(expected, merged)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/NullSentinelPassthroughTest.cs b/csharp/client/Dh_NetClientTests/NullSentinelPassthroughTest.cs index 5ff37e8deff..8526bbaf268 100644 --- a/csharp/client/Dh_NetClientTests/NullSentinelPassthroughTest.cs +++ b/csharp/client/Dh_NetClientTests/NullSentinelPassthroughTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class NullSentinelPassthroughTest { - [Fact] - public void SentinelsVisible() { + [Test] + public async Task SentinelsVisible() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var manager = ctx.Client.Manager; using var t = manager.EmptyTable(1) @@ -22,13 +22,13 @@ public void SentinelsVisible() { ); var ct = t.ToClientTable(); - AssertHasSentinel(ct, 0, DeephavenConstants.NullChar); - AssertHasSentinel(ct, 1, DeephavenConstants.NullByte); - AssertHasSentinel(ct, 2, DeephavenConstants.NullShort); - AssertHasSentinel(ct, 3, DeephavenConstants.NullInt); - AssertHasSentinel(ct, 4, DeephavenConstants.NullLong); - AssertHasSentinel(ct, 5, DeephavenConstants.NullFloat); - AssertHasSentinel(ct, 6, DeephavenConstants.NullDouble); + await Assert.That(() => AssertHasSentinel(ct, 0, DeephavenConstants.NullChar)).ThrowsNothing(); + await Assert.That(() => AssertHasSentinel(ct, 1, DeephavenConstants.NullByte)).ThrowsNothing(); + await Assert.That(() => AssertHasSentinel(ct, 2, DeephavenConstants.NullShort)).ThrowsNothing(); + await Assert.That(() => AssertHasSentinel(ct, 3, DeephavenConstants.NullInt)).ThrowsNothing(); + await Assert.That(() => AssertHasSentinel(ct, 4, DeephavenConstants.NullLong)).ThrowsNothing(); + await Assert.That(() => AssertHasSentinel(ct, 5, DeephavenConstants.NullFloat)).ThrowsNothing(); + await Assert.That(() => AssertHasSentinel(ct, 6, DeephavenConstants.NullDouble)).ThrowsNothing(); } private static void AssertHasSentinel(IClientTable ct, int columnIndex, T sentinel) where T : struct, IEquatable { @@ -52,4 +52,4 @@ private static void AssertHasSentinel(IClientTable ct, int columnIndex, T sen throw new Exception($"For type {Utility.FriendlyTypeName(typeof(T))}, expected value {sentinel}, got {typedChunk.Data[0]}"); } } -} +} \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/ReadOnlyListAdapterTest.cs b/csharp/client/Dh_NetClientTests/ReadOnlyListAdapterTest.cs new file mode 100644 index 00000000000..e50c6afe507 --- /dev/null +++ b/csharp/client/Dh_NetClientTests/ReadOnlyListAdapterTest.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2016-2026 Deephaven Data Labs and Patent Pending +// + +using System.Collections; +using Deephaven.Dh_NetClient; + +namespace Deephaven.Dh_NetClientTests; + +public class ReadOnlyListAdaptersTest { + [Test] + public async Task AsIList() { + IList list = MakeReadOnlyListAdapterForValueTypes(); + await Assert.That(list.Count).IsEqualTo(4); + await Assert.That(list.IsFixedSize).IsTrue(); + await Assert.That(list.IsReadOnly).IsTrue(); + object?[] expected = [1, 2, null, 4]; + await Assert.That(list).IsEquivalentTo(expected); + await Assert.That(list[0]).IsEqualTo(1); + await Assert.That(list.Contains(2)).IsTrue(); + await Assert.That(list.Contains(null)).IsTrue(); + await Assert.That(list.Contains(3)).IsFalse(); + var dest = new object?[5]; + list.CopyTo(dest, 1); + await Assert.That(dest).IsEquivalentTo(new object?[] { null, 1, 2, null, 4 }); + + // test enumeration + var copy = list.Cast().ToArray(); + await Assert.That(list).IsEquivalentTo(copy); + + await Assert.That(() => list.Clear()).Throws(); + await Assert.That(() => list.Add(5)).Throws(); + await Assert.That(() => list.Remove(1)).Throws(); + await Assert.That(() => list.Insert(0, 1)).Throws(); + await Assert.That(() => list.RemoveAt(0)).Throws(); + } + + [Test] + public async Task AsIListOfT() { + IList list = MakeReadOnlyListAdapterForValueTypes(); + await Assert.That(list.Count).IsEqualTo(4); + await Assert.That(list.IsReadOnly).IsTrue(); + Int32[] expected = [1, 2, DeephavenConstants.NullInt, 4]; + await Assert.That(list).IsEquivalentTo(expected.ToArray()); + await Assert.That(list[0]).IsEqualTo(1); + await Assert.That(list.Contains(2)).IsTrue(); + await Assert.That(list.Contains(3)).IsFalse(); + var dest = new int[5]; + list.CopyTo(dest, 1); + await Assert.That(dest).IsEquivalentTo(new Int32[] { 0, 1, 2, DeephavenConstants.NullInt, 4 }); + + // test enumeration + var copy = list.Select(x => x).ToArray(); + await Assert.That(list).IsEquivalentTo(copy); + + await Assert.That(() => list.Clear()).Throws(); + await Assert.That(() => list.Add(5)).Throws(); + await Assert.That(() => list.Remove(1)).Throws(); + await Assert.That(() => list.Insert(0, 1)).Throws(); + await Assert.That(() => list.RemoveAt(0)).Throws(); + } + + [Test] + public async Task AsIListOfNullableT() { + IList list = MakeReadOnlyListAdapterForValueTypes(); + await Assert.That(list.Count).IsEqualTo(4); + await Assert.That(list.IsReadOnly).IsTrue(); + Int32?[] expected = [1, 2, null, 4]; + await Assert.That(list).IsEquivalentTo(expected); + await Assert.That(list[0]).IsEqualTo(1); + await Assert.That(list.Contains(2)).IsTrue(); + await Assert.That(list.Contains(3)).IsFalse(); + var dest = new Int32?[5]; + list.CopyTo(dest, 1); + await Assert.That(dest).IsEquivalentTo(new Int32?[] { null, 1, 2, null, 4 }); + + // test enumeration + var copy = list.Select(x => x).ToArray(); + await Assert.That(list).IsEquivalentTo(copy); + + await Assert.That(() => list.Clear()).Throws(); + await Assert.That(() => list.Add(5)).Throws(); + await Assert.That(() => list.Remove(1)).Throws(); + await Assert.That(() => list.Insert(0, 1)).Throws(); + await Assert.That(() => list.RemoveAt(0)).Throws(); + } + + private static ReadOnlyListAdapterForValueTypes MakeReadOnlyListAdapterForValueTypes() { + var int32Data = new Int32?[] { 1, 2, null, 4 }; + var adapter = new ReadOnlyListAdapterForValueTypes(int32Data, DeephavenConstants.NullInt); + return adapter; + } +} diff --git a/csharp/client/Dh_NetClientTests/RoundTripTest.cs b/csharp/client/Dh_NetClientTests/RoundTripTest.cs index ff72778e10e..cd8f5e79031 100644 --- a/csharp/client/Dh_NetClientTests/RoundTripTest.cs +++ b/csharp/client/Dh_NetClientTests/RoundTripTest.cs @@ -3,8 +3,8 @@ namespace Deephaven.Dh_NetClientTests; public class RoundTripTest { - [Fact] - public void Elements() { + [Test] + public async Task Elements() { var tm = new TableMaker(); tm.AddColumn("Col1", [0, 1, 2, 3, 4, 5]); tm.AddColumn("Col2", ["a", "b", "c", "d", "e", "f"]); @@ -12,23 +12,32 @@ public void Elements() { var at = tm.ToArrowTable(); var ct = ArrowUtil.ToClientTable(at); var at2 = ct.ToArrowTable(); - TableComparer.AssertSame(at, at2); + await Assert.That(() => TableComparer.AssertSame(at, at2)).ThrowsNothing(); } - [Fact] - public void Nested() { + [Test] + public async Task Nested() { var tm = new TableMaker(); - tm.AddColumn("Col1", [[0, 1, 2], [3, 4], [5]]); + var dto1 = new DateTimeOffset(1966, 3, 1, 12, 34, 56, TimeSpan.Zero); + var dto2 = new DateTimeOffset(1999, 12, 31, 3, 44, 55, TimeSpan.Zero); - var at = tm.ToArrowTable(); + tm.AddColumn("Int8", [[0, 1, 2], [3, 4, null], null, [6]]); + tm.AddColumn("Int16", [[0, 1, 2], [3, 4, null], null, [6]]); + tm.AddColumn("Int32", [[0, 1, 2], [3, 4, null], null, [6]]); + tm.AddColumn("Int64", [[0, 1, 2], [3, 4, null], null, [6]]); + tm.AddColumn("float", [[0.0f, 1.1f, 2.2f], [3.3f, 4.4f, null], null, [6.6f]]); + tm.AddColumn("double", [[0.0, 1.1, 2.2], [3.3, 4.4, null], null, [6.6]]); + tm.AddColumn("bool", [[false, true], [false, true, null], null, [true]]); + tm.AddColumn("string", [["", "hello"], ["a", "b", null], null, ["c"]]); + tm.AddColumn("char", [['a', 'X'], ['a', 'b', null], null, ['c']]); + tm.AddColumn("DateTimeOffset", [[dto1, dto2], [DateTimeOffset.MinValue, DateTimeOffset.MaxValue, null], null, [dto2]]); + tm.AddColumn("DateOnly", [[DateOnly.FromDateTime(dto1.Date), DateOnly.FromDateTime(dto2.Date)], [DateOnly.MinValue, DateOnly.MaxValue, null], null, [DateOnly.FromDateTime(dto2.Date)]]); + tm.AddColumn("TimeOnly", [[TimeOnly.FromDateTime(dto1.Date), TimeOnly.FromDateTime(dto2.Date)], [TimeOnly.MinValue, TimeOnly.MaxValue, null], null, [TimeOnly.FromDateTime(dto2.Date)]]); - // TODO(kosak): When you fix this, update DH-19910 - var ex = Assert.Throws(() => { - var ct = ArrowUtil.ToClientTable(at); - var at2 = ct.ToArrowTable(); - TableComparer.AssertSame(at, at2); - }); + var at = tm.ToArrowTable(); - Assert.Contains("Arrow type list is not supported", ex.Message); + var ct = ArrowUtil.ToClientTable(at); + var at2 = ct.ToArrowTable(); + await Assert.That(() => TableComparer.AssertSame(at, at2)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/ScriptTest.cs b/csharp/client/Dh_NetClientTests/ScriptTest.cs index 2e9575c8b51..ab20246a699 100644 --- a/csharp/client/Dh_NetClientTests/ScriptTest.cs +++ b/csharp/client/Dh_NetClientTests/ScriptTest.cs @@ -6,18 +6,18 @@ namespace Deephaven.Dh_NetClientTests; public class ScriptTest { - [Fact] - public void TestScriptSessionError() { + [Test] + public async Task TestScriptSessionError() { using var ctx = CommonContextForTests.Create(new ClientOptions().SetSessionType("")); var m = ctx.Client.Manager; const string script = "from deephaven import empty_table"; - var ex = Record.Exception(() => m.RunScript(script)); - Assert.NotNull(ex); - Assert.Contains("Client was created without specifying a script language", ex.Message); + await Assert.That(() => m.RunScript(script)) + .Throws() + .WithMessageContaining("Client was created without specifying a script language"); } - [Fact] - public void TestScriptExecution() { + [Test] + public async Task TestScriptExecution() { var intData = new List(); var longData = new List(); @@ -42,6 +42,6 @@ from deephaven import empty_table var expected = new TableMaker(); expected.AddColumn("intData", intData); expected.AddColumn("longData", longData); - TableComparer.AssertSame(expected, t); + await Assert.That(() => TableComparer.AssertSame(expected, t)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/SelectTest.cs b/csharp/client/Dh_NetClientTests/SelectTest.cs index 5d07b7edffa..5326903c4a0 100644 --- a/csharp/client/Dh_NetClientTests/SelectTest.cs +++ b/csharp/client/Dh_NetClientTests/SelectTest.cs @@ -2,19 +2,12 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; public class SelectTest { - private readonly ITestOutputHelper _output; - - public SelectTest(ITestOutputHelper output) { - _output = output; - } - - [Fact] - public void TestSupportAllTypes() { + [Test] + public async Task TestSupportAllTypes() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var boolData = new List(); @@ -62,13 +55,13 @@ public void TestSupportAllTypes() { tm.AddColumn("timeOnlyData", timeOnlyData); var th = tm.MakeTable(ctx.Client.Manager); - _output.WriteLine(th.ToString(true)); + Console.WriteLine(th.ToString(true)); - TableComparer.AssertSame(tm, th); + await Assert.That(() => TableComparer.AssertSame(tm, th)).ThrowsNothing(); } - [Fact] - public void TestCreateUpdateFetchATable() { + [Test] + public async Task TestCreateUpdateFetchATable() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = new TableMaker(); @@ -86,11 +79,11 @@ public void TestCreateUpdateFetchATable() { tm.AddColumn("Q2", [0, 100, 200, 300, 400, 500, 600, 700, 800, 900]); tm.AddColumn("Q3", [10, 110, 210, 310, 410, 510, 610, 710, 810, 910]); tm.AddColumn("Q4", [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]); - TableComparer.AssertSame(tm, t4); + await Assert.That(() => TableComparer.AssertSame(tm, t4)).ThrowsNothing(); } - [Fact] - public void TestSelectAFewColumns() { + [Test] + public async Task TestSelectAFewColumns() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; @@ -102,29 +95,28 @@ public void TestSelectAFewColumns() { tm.AddColumn("Ticker", ["AAPL", "AAPL"]); tm.AddColumn("Close", [23.5, 24.2]); tm.AddColumn("Volume", [(Int64)100000, 250000]); - TableComparer.AssertSame(tm, t1); + await Assert.That(() => TableComparer.AssertSame(tm, t1)).ThrowsNothing(); } - - [Fact] - public void TestLastByAndSelect() { + [Test] + public async Task TestLastByAndSelect() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; var t1 = table.Where("ImportDate == `2017-11-01` && Ticker == `AAPL`") .LastBy("Ticker") .Select("Ticker", "Close", "Volume"); - _output.WriteLine(t1.ToString(true)); + Console.WriteLine(t1.ToString(true)); var tm = new TableMaker(); tm.AddColumn("Ticker", ["AAPL"]); tm.AddColumn("Close", [26.7]); tm.AddColumn("Volume", [(Int64)19000]); - TableComparer.AssertSame(tm, t1); + await Assert.That(() => TableComparer.AssertSame(tm, t1)).ThrowsNothing(); } - [Fact] - public void TestNewColumns() { + [Test] + public async Task TestNewColumns() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; @@ -135,23 +127,21 @@ public void TestNewColumns() { var tm = new TableMaker(); tm.AddColumn("MV1", [(double)2350000, 6050000, 507300]); tm.AddColumn("V_plus_12", [(Int64)100012, 250012, 19012]); - TableComparer.AssertSame(tm, t1); + await Assert.That(() => TableComparer.AssertSame(tm, t1)).ThrowsNothing(); } - - [Fact] - public void TestDropColumns() { + [Test] + public async Task TestDropColumns() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; var t1 = table.DropColumns("ImportDate", "Open", "Close"); - Assert.Equal(5, table.Schema.FieldsList.Count); - Assert.Equal(2, t1.Schema.FieldsList.Count); + await Assert.That(table.Schema.FieldsList.Count).IsEqualTo(5); + await Assert.That(t1.Schema.FieldsList.Count).IsEqualTo(2); } - - [Fact] - public void TestSimpleWhere() { + [Test] + public async Task TestSimpleWhere() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; var updated = table.Update("QQQ = i"); @@ -162,38 +152,38 @@ public void TestSimpleWhere() { var tm = new TableMaker(); tm.AddColumn("Ticker", ["IBM"]); tm.AddColumn("Volume", [(Int64)138000]); - TableComparer.AssertSame(tm, t1); + await Assert.That(() => TableComparer.AssertSame(tm, t1)).ThrowsNothing(); } - [Fact] - public void TestFormulaInTheWhereClause() { + [Test] + public async Task TestFormulaInTheWhereClause() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; var t1 = table.Where( "ImportDate == `2017-11-01` && Ticker == `AAPL` && Volume % 10 == Volume % 100") .Select("Ticker", "Volume"); - _output.WriteLine(t1.ToString(true)); + Console.WriteLine(t1.ToString(true)); var tm = new TableMaker(); tm.AddColumn("Ticker", ["AAPL", "AAPL", "AAPL"]); tm.AddColumn("Volume", [(Int64)100000, 250000, 19000]); - TableComparer.AssertSame(tm, t1); + await Assert.That(() => TableComparer.AssertSame(tm, t1)).ThrowsNothing(); } - [Fact] - public void TestSimpleWhereWithSyntaxError() { + [Test] + public async Task TestSimpleWhereWithSyntaxError() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; - Assert.Throws(() => { + await Assert.That(() => { var t1 = table.Where(")))))"); - _output.WriteLine(t1.ToString(true)); - }); + Console.WriteLine(t1.ToString(true)); + }).Throws(); } - [Fact] - public void TestWhereIn() { + [Test] + public async Task TestWhereIn() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var sourceMaker = new TableMaker(); @@ -215,11 +205,11 @@ public void TestWhereIn() { expected.AddColumn("Number", [(Int32?)null, 2, null, 3]); expected.AddColumn("Color", ["red", "blue", "purple", "blue"]); expected.AddColumn("Code", [(Int32?)12, 13, null, null]); - TableComparer.AssertSame(expected, result); + await Assert.That(() => TableComparer.AssertSame(expected, result)).ThrowsNothing(); } - [Fact] - public void TestLazyUpdate() { + [Test] + public async Task TestLazyUpdate() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var sourceMaker = new TableMaker(); @@ -235,11 +225,11 @@ public void TestLazyUpdate() { expected.AddColumn("B", [1, 2, 3, 4]); expected.AddColumn("C", [5, 2, 5, 5]); expected.AddColumn("Y", [Math.Sqrt(5), Math.Sqrt(2), Math.Sqrt(5), Math.Sqrt(5)]); - TableComparer.AssertSame(expected, result); + await Assert.That(() => TableComparer.AssertSame(expected, result)).ThrowsNothing(); } - [Fact] - public void TestSelectDistinct() { + [Test] + public async Task TestSelectDistinct() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var sourceMaker = new TableMaker(); @@ -248,10 +238,10 @@ public void TestSelectDistinct() { var source = sourceMaker.MakeTable(ctx.Client.Manager); var result = source.SelectDistinct("A"); - _output.WriteLine(result.ToString(true)); + Console.WriteLine(result.ToString(true)); var expected = new TableMaker(); expected.AddColumn("A", ["apple", "orange", "plum", "grape"]); - TableComparer.AssertSame(expected, result); + await Assert.That(() => TableComparer.AssertSame(expected, result)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/SharableDictTest.cs b/csharp/client/Dh_NetClientTests/SharableDictTest.cs index 008a91cde73..7838223099d 100644 --- a/csharp/client/Dh_NetClientTests/SharableDictTest.cs +++ b/csharp/client/Dh_NetClientTests/SharableDictTest.cs @@ -7,37 +7,37 @@ namespace Deephaven.Dh_NetClientTests; public class SharableDictTest { - [Fact] - public void Simple() { + [Test] + public async Task Simple() { var d = SharableDict.Empty; var dict = d.With(10, "hello") .With(11, "world") .With(1000, "Deephaven"); - Assert.True(ContainsEntry(dict, 10, "hello")); - Assert.True(ContainsEntry(dict, 11, "world")); - Assert.True(ContainsEntry(dict, 1000, "Deephaven")); - Assert.False(dict.TryGetValue(1001, out _)); - Assert.Equal(3, dict.Count); + await Assert.That(ContainsEntry(dict, 10, "hello")).IsTrue(); + await Assert.That(ContainsEntry(dict, 11, "world")).IsTrue(); + await Assert.That(ContainsEntry(dict, 1000, "Deephaven")).IsTrue(); + await Assert.That(dict.TryGetValue(1001, out _)).IsFalse(); + await Assert.That(dict.Count).IsEqualTo(3); var dict2 = dict.With(11, "world v2") .With(1000, "Deephaven v2"); // dict2 has some new values - Assert.True(ContainsEntry(dict2, 10, "hello")); - Assert.True(ContainsEntry(dict2, 11, "world v2")); - Assert.True(ContainsEntry(dict2, 1000, "Deephaven v2")); - Assert.Equal(3, dict2.Count); + await Assert.That(ContainsEntry(dict2, 10, "hello")).IsTrue(); + await Assert.That(ContainsEntry(dict2, 11, "world v2")).IsTrue(); + await Assert.That(ContainsEntry(dict2, 1000, "Deephaven v2")).IsTrue(); + await Assert.That(dict2.Count).IsEqualTo(3); // Initial dict unchanged - Assert.True(ContainsEntry(dict, 10, "hello")); - Assert.True(ContainsEntry(dict, 11, "world")); - Assert.True(ContainsEntry(dict, 1000, "Deephaven")); - Assert.Equal(3, dict.Count); + await Assert.That(ContainsEntry(dict, 10, "hello")).IsTrue(); + await Assert.That(ContainsEntry(dict, 11, "world")).IsTrue(); + await Assert.That(ContainsEntry(dict, 1000, "Deephaven")).IsTrue(); + await Assert.That(dict.Count).IsEqualTo(3); } - [Fact] - public void Ordering() { + [Test] + public async Task Ordering() { var dict = SharableDict.Empty; dict = dict.With(3, 1000) .With(10, 2000) @@ -52,30 +52,30 @@ public void Ordering() { new(10, 2000) }; - Assert.Equal(expected, list); + await Assert.That(list).IsEquivalentTo(expected); } - [Fact] - public void Canonicalizes() { + [Test] + public async Task Canonicalizes() { var d0 = SharableDict.Empty; var d1 = d0.With(1_000, "hello"); var d2 = d1.With(1_000_000, "there"); var d3 = d2.With(1_000_000_000, "Deephaven"); - Assert.Equal(0, d0.Count); - Assert.Equal(1, d1.Count); - Assert.Equal(2, d2.Count); - Assert.Equal(3, d3.Count); + await Assert.That(d0.Count).IsEqualTo(0); + await Assert.That(d1.Count).IsEqualTo(1); + await Assert.That(d2.Count).IsEqualTo(2); + await Assert.That(d3.Count).IsEqualTo(3); var newd2 = d3.Without(1_000); var newd1 = newd2.Without(1_000_000); var newd0 = newd1.Without(1_000_000_000); - Assert.True(ReferenceEquals(newd0.RootForUnitTests, d0.RootForUnitTests)); + await Assert.That(ReferenceEquals(newd0.RootForUnitTests, d0.RootForUnitTests)).IsTrue(); } - [Fact] - public void NonexistentRemoveIsNoop() { + [Test] + public async Task NonexistentRemoveIsNoop() { var dict = SharableDict.Empty .With(10, "hello") .With(11, "world") @@ -83,11 +83,11 @@ public void NonexistentRemoveIsNoop() { var d2 = dict.Without(999); // nonexistent key - Assert.True(ReferenceEquals(dict, d2)); + await Assert.That(ReferenceEquals(dict, d2)).IsTrue(); } - [Fact] - public void Iterates() { + [Test] + public async Task Iterates() { var dict = SharableDict.Empty; for (var i = 0; i != 10000; ++i) { dict = dict.With(i * 37, "hello" + i); @@ -95,26 +95,26 @@ public void Iterates() { var nextIndex = 0; foreach (var (k, v) in dict) { - Assert.Equal(nextIndex * 37, k); - Assert.Equal("hello" + nextIndex, v); + await Assert.That(k).IsEqualTo(nextIndex * 37); + await Assert.That(v).IsEqualTo("hello" + nextIndex); ++nextIndex; } } - [Fact] - public void Difference() { + [Test] + public async Task Difference() { var dict1 = SharableDict.Empty; for (var i = 0; i != 10; ++i) { dict1 = dict1.With(i, i * 37); } var dict2 = dict1 - .With(100, 999) // add - .With(1000, 9999) // add - .Without(3) // remove - .Without(5) // remove - .With(7, 12345) // modify - .With(-1, 999); // add + .With(100, 999) // add + .With(1000, 9999) // add + .Without(3) // remove + .Without(5) // remove + .With(7, 12345) // modify + .With(-1, 999); // add var (a, r, m) = dict1.CalcDifference(dict2); @@ -133,50 +133,50 @@ public void Difference() { new(7, 12345) }; - Assert.Equal(aExpected, a); - Assert.Equal(rExpected, r); - Assert.Equal(mExpected, m); + await Assert.That(a).IsEquivalentTo(aExpected); + await Assert.That(r).IsEquivalentTo(rExpected); + await Assert.That(m).IsEquivalentTo(mExpected); - Assert.Equal(32, a.CountNodesForUnitTesting()); - Assert.Equal(21, r.CountNodesForUnitTesting()); - Assert.Equal(21, m.CountNodesForUnitTesting()); + await Assert.That(a.CountNodesForUnitTesting()).IsEqualTo(32); + await Assert.That(r.CountNodesForUnitTesting()).IsEqualTo(21); + await Assert.That(m.CountNodesForUnitTesting()).IsEqualTo(21); } - [Fact] - public void DictIsEfficientForLargeDenseSets() { + [Test] + public async Task DictIsEfficientForLargeDenseSets() { // These should asymptote towards 64 elements per node. // An empty dict costs 11 nodes - TestDenseEfficiency(0, 11); + await TestDenseEfficiency(0, 11); // A dict densely packed with the first 64 integers costs 21 nodes // Efficency: 21 nodes per 64 elements // 0.328 nodes per element, 3.048 elements per node - TestDenseEfficiency(64, 21); + await TestDenseEfficiency(64, 21); // A dict densely packed with the first 4096 integers costs 84 nodes // Efficency: 84 nodes per 4096 elements // 0.021 nodes per element, 48.76 elements per node - TestDenseEfficiency(4096, 84); + await TestDenseEfficiency(4096, 84); // A dict densely packed with the first 65536 integers costs 1059 nodes // Efficency: 84 nodes per 4096 elements // 0.016 nodes per element, 61.88 elements per node - TestDenseEfficiency(65536, 1059); + await TestDenseEfficiency(65536, 1059); } - private static void TestDenseEfficiency(int count, int expectedNodeCount) { + private static async Task TestDenseEfficiency(int count, int expectedNodeCount) { var dict = SharableDict.Empty; for (var i = 0; i != count; ++i) { dict = dict.With(i, i * 1111); } for (var i = 0; i != count; ++i) { - Assert.True(dict.TryGetValue(i, out var value)); - Assert.Equal(i * 1111, value); + await Assert.That(dict.TryGetValue(i, out var value)).IsTrue(); + await Assert.That(value).IsEqualTo(i * 1111); } - Assert.Equal(count, dict.Count); - Assert.Equal(expectedNodeCount, dict.CountNodesForUnitTesting()); + await Assert.That(dict.Count).IsEqualTo(count); + await Assert.That(dict.CountNodesForUnitTesting()).IsEqualTo(expectedNodeCount); } private static bool ContainsEntry(SharableDict dict, Int64 key, T expected) { diff --git a/csharp/client/Dh_NetClientTests/SortTest.cs b/csharp/client/Dh_NetClientTests/SortTest.cs index e2965b46639..c205e246ec5 100644 --- a/csharp/client/Dh_NetClientTests/SortTest.cs +++ b/csharp/client/Dh_NetClientTests/SortTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class SortTest { - [Fact] - public void SortDemoTable() { + [Test] + public async Task SortDemoTable() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var testTable = ctx.TestTable; @@ -23,11 +23,11 @@ public void SortDemoTable() { expected.AddColumn("Open", [541.2, 685.3, 92.3, 50.5, 83.1]); expected.AddColumn("Volume", [(Int64)46123, 48300, 6060842, 87000, 345000]); - TableComparer.AssertSame(expected, table1); + await Assert.That(() => TableComparer.AssertSame(expected, table1)).ThrowsNothing(); } - [Fact] - public void SortTempTable() { + [Test] + public async Task SortTempTable() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var maker = new TableMaker(); @@ -46,6 +46,6 @@ public void SortTempTable() { expected.AddColumn("IntValue2", [2, 2, 2, 2, 3, 3, 3, 3, 0, 0, 0, 0, 1, 1, 1, 1]); expected.AddColumn("IntValue3", [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]); - TableComparer.AssertSame(expected, sorted); + await Assert.That(() => TableComparer.AssertSame(expected, sorted)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/StringFilterTest.cs b/csharp/client/Dh_NetClientTests/StringFilterTest.cs index 88901de5f01..45674654ff0 100644 --- a/csharp/client/Dh_NetClientTests/StringFilterTest.cs +++ b/csharp/client/Dh_NetClientTests/StringFilterTest.cs @@ -2,11 +2,12 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; +using TUnit.Assertions.Exceptions; namespace Deephaven.Dh_NetClientTests; public class StringFilterTest { - [Fact] + [Test] public void StringFilter() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var testTable = ctx.TestTable; @@ -44,7 +45,7 @@ private static void TestFilter(string description, TableHandle filteredTable, try { TableComparer.AssertSame(expected, filteredTable); } catch (Exception e) { - throw new AggregateException($"While processing {description}", e); + throw new AssertionException($"While processing {description}: {e.Message}", e); } } } diff --git a/csharp/client/Dh_NetClientTests/TableHandleAttributesTest.cs b/csharp/client/Dh_NetClientTests/TableHandleAttributesTest.cs index 0386cd7976f..99ffedfbced 100644 --- a/csharp/client/Dh_NetClientTests/TableHandleAttributesTest.cs +++ b/csharp/client/Dh_NetClientTests/TableHandleAttributesTest.cs @@ -6,31 +6,31 @@ namespace Deephaven.Dh_NetClientTests; public class TableHandleAttributesTest { - [Fact] - public void TableHandleAttributes() { + [Test] + public async Task TableHandleAttributes() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; const Int64 numRows = 37; var t = thm.EmptyTable(numRows).Update("II = ii"); - Assert.Equal(numRows, t.NumRows); - Assert.True(t.IsStatic); + await Assert.That(t.NumRows).IsEqualTo(numRows); + await Assert.That(t.IsStatic).IsTrue(); } - [Fact] - public void TableHandleDynamicAttributes() { + [Test] + public async Task TableHandleDynamicAttributes() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; var t = thm.TimeTable(1_000_000_000).Update("II = ii"); - Assert.False(t.IsStatic); + await Assert.That(t.IsStatic).IsFalse(); } - [Fact] - public void TableHandleCreatedByDoPut() { + [Test] + public async Task TableHandleCreatedByDoPut() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; - Assert.True(table.IsStatic); + await Assert.That(table.IsStatic).IsTrue(); // The columns all have the same size, so look at the source data for any one of them and get its size var expectedSize = ctx.ColumnData.ImportDate.Length; - Assert.Equal(expectedSize, table.NumRows); + await Assert.That(table.NumRows).IsEqualTo(expectedSize); } } diff --git a/csharp/client/Dh_NetClientTests/TableTest.cs b/csharp/client/Dh_NetClientTests/TableTest.cs index 739aff40682..347b28a9c31 100644 --- a/csharp/client/Dh_NetClientTests/TableTest.cs +++ b/csharp/client/Dh_NetClientTests/TableTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class TableTest { - [Fact] - public void FetchTheWholeTable() { + [Test] + public async Task FetchTheWholeTable() { const int target = 10; using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; @@ -76,6 +76,6 @@ public void FetchTheWholeTable() { expected.AddColumn("Strings", strings); expected.AddColumn("DateTimes", dateTimes); - TableComparer.AssertSame(expected, th); + await Assert.That(() => TableComparer.AssertSame(expected, th)).ThrowsNothing(); } } diff --git a/csharp/client/Dh_NetClientTests/TickingTest.cs b/csharp/client/Dh_NetClientTests/TickingTest.cs index 4e22ff85f6e..7de5c8b37f2 100644 --- a/csharp/client/Dh_NetClientTests/TickingTest.cs +++ b/csharp/client/Dh_NetClientTests/TickingTest.cs @@ -1,25 +1,26 @@ // // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // + +using System.Threading.Channels; using Apache.Arrow; using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class TickingTest(ITestOutputHelper output) { - [Fact] - public void EventuallyReaches10Rows() { +public class TickingTest { + [Test] + public async Task EventuallyReaches10Rows() { const Int64 maxRows = 10; using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; using var table = thm.TimeTable(TimeSpan.FromMilliseconds(500)).Update("II = ii"); - var callback = new ReachesNRowsCallback(output, maxRows); + var callback = new ReachesNRowsCallback(maxRows); using var cookie = table.Subscribe(callback); while (true) { - var (done, exception) = callback.WaitForUpdate(); + var (done, exception) = await callback.WaitForUpdateAsync(); if (done) { break; } @@ -29,8 +30,8 @@ public void EventuallyReaches10Rows() { } } - [Fact] - public void AllEventuallyGreaterThan10() { + [Test] + public async Task AllEventuallyGreaterThan10() { const Int64 maxRows = 10; using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; @@ -39,11 +40,11 @@ public void AllEventuallyGreaterThan10() { .View("Key = (long)(ii % 10)", "Value = ii") .LastBy("Key"); - var callback = new AllValuesGreaterThanNCallback(output, maxRows); + var callback = new AllValuesGreaterThanNCallback(maxRows); using var cookie = table.Subscribe(callback); while (true) { - var (done, exception) = callback.WaitForUpdate(); + var (done, exception) = await callback.WaitForUpdateAsync(); if (done) { break; } @@ -53,8 +54,8 @@ public void AllEventuallyGreaterThan10() { } } - [Fact] - public void AllDataEventuallyPresent() { + [Test] + public async Task AllDataEventuallyPresent() { const Int64 maxRows = 10; using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; @@ -76,11 +77,11 @@ public void AllDataEventuallyPresent() { .Sort(SortPair.Ascending("II")) .DropColumns("Timestamp", "II"); - var callback = new WaitForPopulatedTableCallback(output, maxRows); + var callback = new WaitForPopulatedTableCallback(maxRows); using var cookie = table.Subscribe(callback); while (true) { - var (done, exception) = callback.WaitForUpdate(); + var (done, exception) = await callback.WaitForUpdateAsync(); if (done) { break; } @@ -92,58 +93,36 @@ public void AllDataEventuallyPresent() { } public abstract class CommonBase : IObserver { - protected readonly ITestOutputHelper Output; - - protected CommonBase(ITestOutputHelper output) { - Output = output; - } - - private readonly object _sync = new(); - private bool _done = false; - private Exception? _exception = null; + private readonly Channel<(bool, Exception?)> _channel = Channel.CreateUnbounded<(bool, Exception?)>(); public void OnError(Exception error) { - lock (_sync) { - _exception = error; - Monitor.PulseAll(_sync); - } + _channel.Writer.TryWrite((false, error)); } - public (bool, Exception?) WaitForUpdate() { - lock (_sync) { - while (true) { - if (_done || _exception != null) { - return (_done, _exception); - } - - Monitor.Wait(_sync); - } - } + public async Task<(bool, Exception?)> WaitForUpdateAsync() { + return await _channel.Reader.ReadAsync(); } public void OnCompleted() { - Output.WriteLine("Subscription complete"); + Console.WriteLine("Subscription complete"); } public abstract void OnNext(TickingUpdate value); protected void NotifyDone() { - lock (_sync) { - _done = true; - Monitor.PulseAll(_sync); - } + _channel.Writer.TryWrite((true, null)); } } public sealed class ReachesNRowsCallback: CommonBase { private readonly Int64 _target; - public ReachesNRowsCallback(ITestOutputHelper output, Int64 target) : base(output) { + public ReachesNRowsCallback(Int64 target) { _target = target; } public override void OnNext(TickingUpdate update) { - Output.WriteLine($"=== The Full Table ===\n{update.Current.ToString(true, true)}"); + Console.WriteLine($"=== The Full Table ===\n{update.Current.ToString(true, true)}"); if (update.Current.NumRows >= _target) { NotifyDone(); } @@ -153,12 +132,12 @@ public override void OnNext(TickingUpdate update) { public sealed class WaitForPopulatedTableCallback : CommonBase { private readonly Int64 _target; - public WaitForPopulatedTableCallback(ITestOutputHelper output, Int64 target) : base(output) { + public WaitForPopulatedTableCallback(Int64 target) { _target = target; } public override void OnNext(TickingUpdate update) { - Output.WriteLine($"=== The Full Table ===\n{update.Current.ToString(true, true)}"); + Console.WriteLine($"=== The Full Table ===\n{update.Current.ToString(true, true)}"); var current = update.Current; @@ -236,14 +215,14 @@ public override void OnNext(TickingUpdate update) { public sealed class AllValuesGreaterThanNCallback : CommonBase { private readonly Int64 _target; - public AllValuesGreaterThanNCallback(ITestOutputHelper output, Int64 target) : base(output) { + public AllValuesGreaterThanNCallback(Int64 target) { _target = target; } public override void OnNext(TickingUpdate update) { var current = update.Current; - Output.WriteLine($"=== The Full Table ===\n{current.ToString(true, true)}"); + Console.WriteLine($"=== The Full Table ===\n{current.ToString(true, true)}"); if (current.NumRows == 0) { return; @@ -256,4 +235,4 @@ public override void OnNext(TickingUpdate update) { NotifyDone(); } } -} +} \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/UngroupTest.cs b/csharp/client/Dh_NetClientTests/UngroupTest.cs index e78d5383d8a..a6bde941d6b 100644 --- a/csharp/client/Dh_NetClientTests/UngroupTest.cs +++ b/csharp/client/Dh_NetClientTests/UngroupTest.cs @@ -2,35 +2,34 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class UngroupTest(ITestOutputHelper output) { - [Fact] - public void UngroupColumns() { +public class UngroupTest { + [Test] + public async Task UngroupColumns() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; table = table.Where("ImportDate == `2017-11-01`"); var byTable = table.Where("Ticker == `AAPL`").View("Ticker", "Close").By("Ticker"); - output.WriteLine(byTable.ToString(true, true)); + Console.WriteLine(byTable.ToString(true, true)); var ungrouped = byTable.Ungroup("Close"); - output.WriteLine(ungrouped.ToString(true, true)); + Console.WriteLine(ungrouped.ToString(true, true)); { var expected = new TableMaker(); expected.AddColumn("Ticker", ["AAPL"]); expected.AddColumn("Close", [[23.5, 24.2, 26.7]]); - TableComparer.AssertSame(expected, byTable); + await Assert.That(() => TableComparer.AssertSame(expected, byTable)).ThrowsNothing(); } { var expected = new TableMaker(); expected.AddColumn("Ticker", ["AAPL", "AAPL", "AAPL"]); expected.AddColumn("Close", [23.5, 24.2, 26.7]); - TableComparer.AssertSame(expected, ungrouped); + await Assert.That(() => TableComparer.AssertSame(expected, ungrouped)).ThrowsNothing(); } } } diff --git a/csharp/client/Dh_NetClientTests/UpdateByTest.cs b/csharp/client/Dh_NetClientTests/UpdateByTest.cs index 0c99f1b5475..e59fccadf32 100644 --- a/csharp/client/Dh_NetClientTests/UpdateByTest.cs +++ b/csharp/client/Dh_NetClientTests/UpdateByTest.cs @@ -2,17 +2,16 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; using static Deephaven.Dh_NetClient.UpdateByOperation; namespace Deephaven.Dh_NetClientTests; -public class UpdateByTest(ITestOutputHelper output) { +public class UpdateByTest() { const int NumCols = 5; const int NumRows = 1000; - [Fact] - public void SimpleCumSum() { + [Test] + public async Task SimpleCumSum() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -22,11 +21,11 @@ public void SimpleCumSum() { var expected = new TableMaker(); expected.AddColumn("SumX", new Int64[] { 0, 1, 2, 4, 6, 9, 12, 16, 20, 25 }); - TableComparer.AssertSame(expected, filtered); + await Assert.That(() => TableComparer.AssertSame(expected, filtered)).ThrowsNothing(); } - [Fact] - public void SimpleOps() { + [Test] + public async Task SimpleOps() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -37,17 +36,17 @@ public void SimpleOps() { var op = simpleOps[opIndex]; for (var tableIndex = 0; tableIndex != tables.Length; ++tableIndex) { var table = tables[tableIndex]; - output.WriteLine($"Processing op {opIndex} on Table {tableIndex}"); + Console.WriteLine($"Processing op {opIndex} on Table {tableIndex}"); using var result = table.UpdateBy([op], "e"); - Assert.Equal(table.IsStatic, result.IsStatic); - Assert.Equal(2 + table.NumCols, result.NumCols); - Assert.True(result.NumRows >= table.NumRows); + await Assert.That(result.IsStatic).IsEqualTo(table.IsStatic); + await Assert.That(result.NumCols).IsEqualTo(2 + table.NumCols); + await Assert.That(result.NumRows >= table.NumRows).IsTrue(); } } } - [Fact] - public void EmOps() { + [Test] + public async Task EmOps() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -58,19 +57,19 @@ public void EmOps() { var op = emOps[opIndex]; for (var tableIndex = 0; tableIndex != tables.Length; ++tableIndex) { var table = tables[tableIndex]; - output.WriteLine($"Processing op {opIndex} on Table {tableIndex}"); + Console.WriteLine($"Processing op {opIndex} on Table {tableIndex}"); using var result = table.UpdateBy([op], "b"); - Assert.Equal(table.IsStatic, result.IsStatic); - Assert.Equal(1 + table.NumCols, result.NumCols); + await Assert.That(result.IsStatic).IsEqualTo(table.IsStatic); + await Assert.That(result.NumCols).IsEqualTo(1 + table.NumCols); if (result.IsStatic) { - Assert.Equal(result.NumRows, table.NumRows); + await Assert.That(table.NumRows).IsEqualTo(result.NumRows); } } } } - [Fact] - public void RollingOps() { + [Test] + public async Task RollingOps() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -81,17 +80,17 @@ public void RollingOps() { var op = rollingOps[opIndex]; for (var tableIndex = 0; tableIndex != tables.Length; ++tableIndex) { var table = tables[tableIndex]; - output.WriteLine($"Processing op {opIndex} on Table {tableIndex}"); + Console.WriteLine($"Processing op {opIndex} on Table {tableIndex}"); using var result = table.UpdateBy([op], "c"); - Assert.Equal(table.IsStatic, result.IsStatic); - Assert.Equal(2 + table.NumCols, result.NumCols); - Assert.True(result.NumRows >= table.NumRows); + await Assert.That(result.IsStatic).IsEqualTo(table.IsStatic); + await Assert.That(result.NumCols).IsEqualTo(2 + table.NumCols); + await Assert.That(result.NumRows >= table.NumRows).IsTrue(); } } } - [Fact] - public void MultipleOps() { + [Test] + public async Task MultipleOps() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var tm = ctx.Client.Manager; @@ -106,12 +105,12 @@ public void MultipleOps() { for (var tableIndex = 0; tableIndex != tables.Length; ++tableIndex) { var table = tables[tableIndex]; - output.WriteLine($"Processing table {tableIndex}"); + Console.WriteLine($"Processing table {tableIndex}"); using var result = table.UpdateBy(multipleOps, "c"); - Assert.Equal(table.IsStatic, result.IsStatic); - Assert.Equal(10 + table.NumCols, result.NumCols); + await Assert.That(result.IsStatic).IsEqualTo(table.IsStatic); + await Assert.That(result.NumCols).IsEqualTo(10 + table.NumCols); if (result.IsStatic) { - Assert.Equal(result.NumRows, table.NumRows); + await Assert.That(table.NumRows).IsEqualTo(result.NumRows); } } } @@ -267,4 +266,4 @@ private static UpdateByOperation[] MakeRollingOps() { }; return result; } -} +} \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/ValidationTest.cs b/csharp/client/Dh_NetClientTests/ValidationTest.cs index f9e3b9e0c11..a5ad8b531a1 100644 --- a/csharp/client/Dh_NetClientTests/ValidationTest.cs +++ b/csharp/client/Dh_NetClientTests/ValidationTest.cs @@ -2,30 +2,27 @@ // Copyright (c) 2016-2025 Deephaven Data Labs and Patent Pending // using Deephaven.Dh_NetClient; -using Xunit.Abstractions; namespace Deephaven.Dh_NetClientTests; -public class ValidationTest(ITestOutputHelper output) { - [Theory] - [InlineData("X = 3)")] // Syntax error - [InlineData("S = `hello`", "T = java.util.regex.Pattern.quote(S)")] // Pattern.quote not on whitelist - [InlineData("X = Math.min(3, 4)")] // Math.min not on whitelist - public void BadSelect(params string[] selections) { +public class ValidationTest { + [Test] + [Arguments("X = 3)")] // Syntax error + [Arguments("S = `hello`", "T = java.util.regex.Pattern.quote(S)")] // Pattern.quote not on whitelist + [Arguments("X = Math.min(3, 4)")] // Math.min not on whitelist + public async Task BadSelect(params string[] selections) { using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; using var staticTable = thm.EmptyTable(10); - Assert.Throws(() => { - using var temp = staticTable.Select(selections); - }); + await Assert.That(() => staticTable.Select(selections)).Throws(); } - [Theory] - [InlineData("X = 3")] - [InlineData("S = `hello`", "T = S.length()")] // instance methods of String ok - [InlineData("X = min(3, 4)")] // builtin from GroovyStaticImports - [InlineData("X = isFinite(3)")] // another builtin from GroovyStaticImports + [Test] + [Arguments("X = 3")] + [Arguments("S = `hello`", "T = S.length()")] // instance methods of String ok + [Arguments("X = min(3, 4)")] // builtin from GroovyStaticImports + [Arguments("X = isFinite(3)")] // another builtin from GroovyStaticImports public void GoodSelect(params string[] selections) { using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; @@ -33,28 +30,26 @@ public void GoodSelect(params string[] selections) { .Select(selections); } - [Theory] - [InlineData("X > 3)")] // syntax error - [InlineData("S = java.util.regex.Pattern.quote(S)")] // Pattern.quote not on whitelist - [InlineData("X = Math.min(3, 4)")] // Math.min not on whitelist - public void BadWhere(string condition) { + [Test] + [Arguments("X > 3)")] // syntax error + [Arguments("S = java.util.regex.Pattern.quote(S)")] // Pattern.quote not on whitelist + [Arguments("X = Math.min(3, 4)")] // Math.min not on whitelist + public async Task BadWhere(string condition) { using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; using var staticTable = thm.EmptyTable(10) .Update("X = 12", "S = `hello`"); - Assert.Throws(() => { - using var temp = staticTable.Where(condition); - }); + await Assert.That(() => staticTable.Where(condition)).Throws(); } - [Theory] - [InlineData("X = 3")] - [InlineData("S = `hello`")] - [InlineData("S.length() = 17")] // instance methods of String ok - [InlineData("X = min(3, 4)")] // "builtin" from GroovyStaticImports - [InlineData("X = isFinite(3)")] // another builtin from GroovyStaticImports - [InlineData("X in 3, 4, 5")] + [Test] + [Arguments("X = 3")] + [Arguments("S = `hello`")] + [Arguments("S.length() = 17")] // instance methods of String ok + [Arguments("X = min(3, 4)")] // "builtin" from GroovyStaticImports + [Arguments("X = isFinite(3)")] // another builtin from GroovyStaticImports + [Arguments("X in 3, 4, 5")] public void GoodWhere(string condition) { using var ctx = CommonContextForTests.Create(new ClientOptions()); var thm = ctx.Client.Manager; @@ -62,4 +57,4 @@ public void GoodWhere(string condition) { .Update("X = 12", "S = `hello`") .Where(condition); } -} +} \ No newline at end of file diff --git a/csharp/client/Dh_NetClientTests/ViewTest.cs b/csharp/client/Dh_NetClientTests/ViewTest.cs index b12df1535cd..60dc049fc89 100644 --- a/csharp/client/Dh_NetClientTests/ViewTest.cs +++ b/csharp/client/Dh_NetClientTests/ViewTest.cs @@ -6,8 +6,8 @@ namespace Deephaven.Dh_NetClientTests; public class ViewTest { - [Fact] - public void View() { + [Test] + public async Task View() { using var ctx = CommonContextForTests.Create(new ClientOptions()); var table = ctx.TestTable; @@ -19,6 +19,6 @@ public void View() { expected.AddColumn("Close", [53.8, 88.5, 38.7, 453, 26.7, 544.9, 13.4]); expected.AddColumn("Volume", [(Int64)87000, 6060842, 138000, 138000000, 19000, 48300, 1500]); - TableComparer.AssertSame(expected, t1); + await Assert.That(() => TableComparer.AssertSame(expected, t1)).ThrowsNothing(); } }