diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fb38cd4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.cs] +charset = utf-8-bom +indent_style = space +indent_size = 2 + +[*.{csproj,props,targets,slnx,json,yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d0d6371 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.png binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a6dac3c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + submodules: recursive + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 + with: + dotnet-version: "10.0.x" + + - name: Restore + run: dotnet restore substrait-csharp.slnx + + - name: Check code style + run: dotnet format substrait-csharp.slnx --verify-no-changes + + - name: Build + run: dotnet build substrait-csharp.slnx --no-restore --configuration Release + + - name: Test + run: dotnet test substrait-csharp.slnx --no-build --configuration Release diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..f118034 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + true + latest-recommended + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..1e7fdfa --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestMinor" + } +} diff --git a/src/Substrait.Core/Relation/Aggregate.cs b/src/Substrait.Core/Relation/Aggregate.cs index f53af86..447729e 100644 --- a/src/Substrait.Core/Relation/Aggregate.cs +++ b/src/Substrait.Core/Relation/Aggregate.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The AGGREGATE relational operator representing GROUP BY semantics, +/// +public class Aggregate : Rel { - /// - /// The AGGREGATE relational operator representing GROUP BY semantics, - /// - public class Aggregate : Rel - { - } } diff --git a/src/Substrait.Core/Relation/Fetch.cs b/src/Substrait.Core/Relation/Fetch.cs index 4ce5e36..74c458e 100644 --- a/src/Substrait.Core/Relation/Fetch.cs +++ b/src/Substrait.Core/Relation/Fetch.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The FETCH relational operator representing LIMIT or TOP semantics, +/// +public class Fetch : Rel { - /// - /// The FETCH relational operator representing LIMIT or TOP semantics, - /// - public class Fetch : Rel - { - } } diff --git a/src/Substrait.Core/Relation/Filter.cs b/src/Substrait.Core/Relation/Filter.cs index d649ddc..33ece77 100644 --- a/src/Substrait.Core/Relation/Filter.cs +++ b/src/Substrait.Core/Relation/Filter.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The FILTER relational operator, +/// +public class Filter : Rel { - /// - /// The FILTER relational operator, - /// - public class Filter : Rel - { - } } diff --git a/src/Substrait.Core/Relation/Join.cs b/src/Substrait.Core/Relation/Join.cs index 0289d8a..006f2c9 100644 --- a/src/Substrait.Core/Relation/Join.cs +++ b/src/Substrait.Core/Relation/Join.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The binary JOIN relational operator, +/// +public class Join : Rel { - /// - /// The binary JOIN relational operator, - /// - public class Join : Rel - { - } } diff --git a/src/Substrait.Core/Relation/Project.cs b/src/Substrait.Core/Relation/Project.cs index a4e5d2a..4cbde0f 100644 --- a/src/Substrait.Core/Relation/Project.cs +++ b/src/Substrait.Core/Relation/Project.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The PROJECT relational operator representing calculated expressions of fields, +/// +public class Project : Rel { - /// - /// The PROJECT relational operator representing calculated expressions of fields, - /// - public class Project : Rel - { - } } diff --git a/src/Substrait.Core/Relation/Read.cs b/src/Substrait.Core/Relation/Read.cs index 869d288..54f59e5 100644 --- a/src/Substrait.Core/Relation/Read.cs +++ b/src/Substrait.Core/Relation/Read.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The READ relational operator representing data scan, +/// +public class Read : Rel { - /// - /// The READ relational operator representing data scan, - /// - public class Read : Rel - { - } } diff --git a/src/Substrait.Core/Relation/Rel.cs b/src/Substrait.Core/Relation/Rel.cs index 8730c30..6d8f2c1 100644 --- a/src/Substrait.Core/Relation/Rel.cs +++ b/src/Substrait.Core/Relation/Rel.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// Base type for all relational operators, +/// +public abstract class Rel { - /// - /// Base type for all relational operators, - /// - abstract public class Rel - { - } } diff --git a/src/Substrait.Core/Relation/Sort.cs b/src/Substrait.Core/Relation/Sort.cs index 991f4b9..41d914e 100644 --- a/src/Substrait.Core/Relation/Sort.cs +++ b/src/Substrait.Core/Relation/Sort.cs @@ -1,9 +1,8 @@ -namespace Substrait.Relation +namespace Substrait.Core.Relation; + +/// +/// The SORT relational operator representing ORDER BY semantics, +/// +public class Sort : Rel { - /// - /// The SORT relational operator representing ORDER BY semantics, - /// - public class Sort : Rel - { - } } diff --git a/src/Substrait.Core/Substrait.Core.csproj b/src/Substrait.Core/Substrait.Core.csproj index 3b23017..1be2e78 100644 --- a/src/Substrait.Core/Substrait.Core.csproj +++ b/src/Substrait.Core/Substrait.Core.csproj @@ -1,20 +1,20 @@  - net6.0 + net10.0 enable enable True - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Substrait.Core/SubstraitRelVisitor.cs b/src/Substrait.Core/SubstraitRelVisitor.cs index 3a48f12..ca0b580 100644 --- a/src/Substrait.Core/SubstraitRelVisitor.cs +++ b/src/Substrait.Core/SubstraitRelVisitor.cs @@ -1,107 +1,92 @@ -using Substrait.Relation; +using Substrait.Core.Relation; -namespace Substrait.Core +namespace Substrait.Core; + +/// +/// Visitor to transform, compile, and/or process SQL logical operators represented using Substrait. The visitor has +/// methods for visiting relation operator objects as input and provides concrete implementation to meet its goals. +/// +public abstract class SubstraitRelVisitor { /// - /// Visitor to transform, compile, and/or process SQL logical operators represented using Substrait. The visitor has - /// methods for visiting relation operator objects as input and provides concrete implementation to meet its goals. + /// Visit relational operator of type AGGREGATE + /// + public virtual void Visit(Aggregate aggregate) + { + ArgumentNullException.ThrowIfNull(aggregate); + + Fallback(aggregate); + } + + /// + /// Visit relational operator of type FETCH + /// + public virtual void Visit(Fetch fetch) + { + ArgumentNullException.ThrowIfNull(fetch); + + Fallback(fetch); + } + + /// + /// Visit relational operator of type FILTER + /// + public virtual void Visit(Filter filter) + { + ArgumentNullException.ThrowIfNull(filter); + + Fallback(filter); + } + + /// + /// Visit relational operator of type JOIN + /// + public virtual void Visit(Join join) + { + ArgumentNullException.ThrowIfNull(join); + + Fallback(join); + } + + /// + /// Visit relational operator of type PROJECT + /// + public virtual void Visit(Project project) + { + ArgumentNullException.ThrowIfNull(project); + + Fallback(project); + } + + /// + /// Visit relational operator of type READ + /// + public virtual void Visit(Read read) + { + ArgumentNullException.ThrowIfNull(read); + + Fallback(read); + } + + /// + /// Visit relational operator of type SORT /// - public abstract class SubstraitRelVisitor + public virtual void Visit(Sort sort) { - /// - /// Visit relational operator of type AGGREGATE - /// - public void Visit(Aggregate aggregate) - { - if (aggregate is null) - { - throw new ArgumentNullException(nameof(aggregate)); - } - - Fallback(aggregate); - } - - /// - /// Visit relational operator of type FETCH - /// - public void Visit(Fetch fetch) - { - if (fetch is null) - { - throw new ArgumentNullException(nameof(fetch)); - } - - Fallback(fetch); - } - - /// - /// Visit relational operator of type FILTER - /// - public void Visit(Filter filter) - { - if (filter is null) - { - throw new ArgumentNullException(nameof(filter)); - } - - Fallback(filter); - } - - /// - /// Visit relational operator of type JOIN - /// - public void Visit(Join join) - { - if (join is null) - { - throw new ArgumentNullException(nameof(join)); - } - - Fallback(join); - } - - /// - /// Visit relational operator of type PROJECT - /// - public void Visit(Project project) - { - if (project is null) - { - throw new ArgumentNullException(nameof(project)); - } - - Fallback(project); - } - - /// - /// Visit relational operator of type READ - /// - public void Visit(Read read) - { - if (read is null) - { - throw new ArgumentNullException(nameof(read)); - } - - Fallback(read); - } - - /// - /// Visit relational operator of type SORT - /// - public void Visit(Sort sort) - { - if (sort is null) - { - throw new ArgumentNullException(nameof(sort)); - } - - Fallback(sort); - } - - public void Fallback(Rel _) - { - throw new InvalidOperationException(); - } + ArgumentNullException.ThrowIfNull(sort); + + Fallback(sort); + } + + /// + /// Invoked for any relational operator the visitor does not handle explicitly. Override to supply + /// default behaviour. + /// + protected virtual void Fallback(Rel rel) + { + ArgumentNullException.ThrowIfNull(rel); + + throw new InvalidOperationException( + $"{GetType().Name} does not handle relational operator '{rel.GetType().Name}'."); } } diff --git a/src/Substrait.Core/Types/Compound/Decimal.cs b/src/Substrait.Core/Types/Compound/Decimal.cs new file mode 100644 index 0000000..0153918 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/Decimal.cs @@ -0,0 +1,10 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record Decimal : TypeClass +{ + public required int Precision { get; init; } + + public required int Scale { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/FixedBinary.cs b/src/Substrait.Core/Types/Compound/FixedBinary.cs new file mode 100644 index 0000000..12996d9 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/FixedBinary.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record FixedBinary : TypeClass +{ + public required int Length { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/FixedChar.cs b/src/Substrait.Core/Types/Compound/FixedChar.cs new file mode 100644 index 0000000..0fbfc4b --- /dev/null +++ b/src/Substrait.Core/Types/Compound/FixedChar.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record FixedChar : TypeClass +{ + public required int Length { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/Func.cs b/src/Substrait.Core/Types/Compound/Func.cs new file mode 100644 index 0000000..a569062 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/Func.cs @@ -0,0 +1,36 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record Func : TypeClass +{ + private readonly TypeClass[] _parameterTypes = []; + + public required IReadOnlyList ParameterTypes + { + get => _parameterTypes; + init => _parameterTypes = value.ToArray(); + } + + public required TypeClass ReturnType { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); + + public bool Equals(Func? other) => + ReferenceEquals(this, other) + || (other is not null + && Nullable == other.Nullable + && ReturnType == other.ReturnType + && ParameterTypes.SequenceEqual(other.ParameterTypes)); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Nullable); + hash.Add(ReturnType); + foreach (var parameterType in ParameterTypes) + { + hash.Add(parameterType); + } + + return hash.ToHashCode(); + } +} diff --git a/src/Substrait.Core/Types/Compound/IntervalCompound.cs b/src/Substrait.Core/Types/Compound/IntervalCompound.cs new file mode 100644 index 0000000..52ec347 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/IntervalCompound.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record IntervalCompound : TypeClass +{ + public required int Precision { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/IntervalDay.cs b/src/Substrait.Core/Types/Compound/IntervalDay.cs new file mode 100644 index 0000000..14cef7a --- /dev/null +++ b/src/Substrait.Core/Types/Compound/IntervalDay.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record IntervalDay : TypeClass +{ + public required int Precision { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/List.cs b/src/Substrait.Core/Types/Compound/List.cs new file mode 100644 index 0000000..754eba2 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/List.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record List : TypeClass +{ + public required TypeClass ElementType { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/Map.cs b/src/Substrait.Core/Types/Compound/Map.cs new file mode 100644 index 0000000..e309856 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/Map.cs @@ -0,0 +1,10 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record Map : TypeClass +{ + public required TypeClass Key { get; init; } + + public required TypeClass Value { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/PrecisionTime.cs b/src/Substrait.Core/Types/Compound/PrecisionTime.cs new file mode 100644 index 0000000..aa8fe7c --- /dev/null +++ b/src/Substrait.Core/Types/Compound/PrecisionTime.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record PrecisionTime : TypeClass +{ + public required int Precision { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/PrecisionTimestamp.cs b/src/Substrait.Core/Types/Compound/PrecisionTimestamp.cs new file mode 100644 index 0000000..13f939c --- /dev/null +++ b/src/Substrait.Core/Types/Compound/PrecisionTimestamp.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record PrecisionTimestamp : TypeClass +{ + public required int Precision { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/PrecisionTimestampTz.cs b/src/Substrait.Core/Types/Compound/PrecisionTimestampTz.cs new file mode 100644 index 0000000..74e9b34 --- /dev/null +++ b/src/Substrait.Core/Types/Compound/PrecisionTimestampTz.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record PrecisionTimestampTz : TypeClass +{ + public required int Precision { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Compound/Struct.cs b/src/Substrait.Core/Types/Compound/Struct.cs new file mode 100644 index 0000000..80e3c1d --- /dev/null +++ b/src/Substrait.Core/Types/Compound/Struct.cs @@ -0,0 +1,30 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record Struct : TypeClass +{ + private readonly TypeClass[] _fields = []; + + public required IReadOnlyList Fields + { + get => _fields; + init => _fields = value.ToArray(); + } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); + + public bool Equals(Struct? other) => + ReferenceEquals(this, other) + || (other is not null && Nullable == other.Nullable && Fields.SequenceEqual(other.Fields)); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(Nullable); + foreach (var field in Fields) + { + hash.Add(field); + } + + return hash.ToHashCode(); + } +} diff --git a/src/Substrait.Core/Types/Compound/VarChar.cs b/src/Substrait.Core/Types/Compound/VarChar.cs new file mode 100644 index 0000000..d8a2dfe --- /dev/null +++ b/src/Substrait.Core/Types/Compound/VarChar.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types.Compound; + +public sealed record VarChar : TypeClass +{ + public required int Length { get; init; } + + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Extension/IParameter.cs b/src/Substrait.Core/Types/Extension/IParameter.cs new file mode 100644 index 0000000..9831bb9 --- /dev/null +++ b/src/Substrait.Core/Types/Extension/IParameter.cs @@ -0,0 +1,5 @@ +namespace Substrait.Core.Types.Extension; + +public interface IParameter +{ +} diff --git a/src/Substrait.Core/Types/Extension/ParameterBooleanValue.cs b/src/Substrait.Core/Types/Extension/ParameterBooleanValue.cs new file mode 100644 index 0000000..e87d2b0 --- /dev/null +++ b/src/Substrait.Core/Types/Extension/ParameterBooleanValue.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Extension; + +public sealed record ParameterBooleanValue : IParameter +{ + public required bool Value { get; init; } +} diff --git a/src/Substrait.Core/Types/Extension/ParameterDataType.cs b/src/Substrait.Core/Types/Extension/ParameterDataType.cs new file mode 100644 index 0000000..aa06cde --- /dev/null +++ b/src/Substrait.Core/Types/Extension/ParameterDataType.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Extension; + +public sealed record ParameterDataType : IParameter +{ + public required TypeClass Type { get; init; } +} diff --git a/src/Substrait.Core/Types/Extension/ParameterEnumValue.cs b/src/Substrait.Core/Types/Extension/ParameterEnumValue.cs new file mode 100644 index 0000000..7a0f654 --- /dev/null +++ b/src/Substrait.Core/Types/Extension/ParameterEnumValue.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Extension; + +public sealed record ParameterEnumValue : IParameter +{ + public required string Value { get; init; } +} diff --git a/src/Substrait.Core/Types/Extension/ParameterIntegerValue.cs b/src/Substrait.Core/Types/Extension/ParameterIntegerValue.cs new file mode 100644 index 0000000..96319ee --- /dev/null +++ b/src/Substrait.Core/Types/Extension/ParameterIntegerValue.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Extension; + +public sealed record ParameterIntegerValue : IParameter +{ + public required long Value { get; init; } +} diff --git a/src/Substrait.Core/Types/Extension/ParameterNull.cs b/src/Substrait.Core/Types/Extension/ParameterNull.cs new file mode 100644 index 0000000..fe7b324 --- /dev/null +++ b/src/Substrait.Core/Types/Extension/ParameterNull.cs @@ -0,0 +1,10 @@ +namespace Substrait.Core.Types.Extension; + +public sealed record ParameterNull : IParameter +{ + public static readonly ParameterNull Instance = new(); + + private ParameterNull() + { + } +} diff --git a/src/Substrait.Core/Types/Extension/ParameterStringValue.cs b/src/Substrait.Core/Types/Extension/ParameterStringValue.cs new file mode 100644 index 0000000..75daf6f --- /dev/null +++ b/src/Substrait.Core/Types/Extension/ParameterStringValue.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Extension; + +public sealed record ParameterStringValue : IParameter +{ + public required string Value { get; init; } +} diff --git a/src/Substrait.Core/Types/Extension/UserDefined.cs b/src/Substrait.Core/Types/Extension/UserDefined.cs new file mode 100644 index 0000000..f9f9215 --- /dev/null +++ b/src/Substrait.Core/Types/Extension/UserDefined.cs @@ -0,0 +1,37 @@ +// namespace Substrait.Core.Types.Extension; +// +// public sealed record UserDefined : TypeClass +// { +// public required string Urn { get; init; } +// +// public required string Name { get; init; } +// +// public IReadOnlyList TypeParameters { get; init; } = Array.Empty(); +// +// public int TypeVariationReference { get; init; } +// +// public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +// +// public bool Equals(UserDefined? other) => +// other is not null +// && Nullable == other.Nullable +// && Urn == other.Urn +// && Name == other.Name +// && TypeVariationReference == other.TypeVariationReference +// && TypeParameters.SequenceEqual(other.TypeParameters); +// +// public override int GetHashCode() +// { +// var hash = new HashCode(); +// hash.Add(Nullable); +// hash.Add(Urn); +// hash.Add(Name); +// hash.Add(TypeVariationReference); +// foreach (var parameter in TypeParameters) +// { +// hash.Add(parameter); +// } +// +// return hash.ToHashCode(); +// } +// } diff --git a/src/Substrait.Core/Types/ITypeVisitor.cs b/src/Substrait.Core/Types/ITypeVisitor.cs new file mode 100644 index 0000000..7a05666 --- /dev/null +++ b/src/Substrait.Core/Types/ITypeVisitor.cs @@ -0,0 +1,67 @@ +using Substrait.Core.Types.Compound; +using Substrait.Core.Types.Simple; +using Decimal = Substrait.Core.Types.Compound.Decimal; +using String = Substrait.Core.Types.Simple.String; + +namespace Substrait.Core.Types; + +public interface ITypeVisitor +{ + /// + /// Invoked for any type kind the implementation does not handle explicitly. Override to supply a + /// default result, or to throw a domain-specific error. + /// + TResult VisitFallback(TypeClass type) => + throw new NotSupportedException( + $"{GetType().Name} does not handle type kind '{type.GetType().Name}'."); + + TResult Visit(Bool type) => VisitFallback(type); + + TResult Visit(I8 type) => VisitFallback(type); + + TResult Visit(I16 type) => VisitFallback(type); + + TResult Visit(I32 type) => VisitFallback(type); + + TResult Visit(I64 type) => VisitFallback(type); + + TResult Visit(Fp32 type) => VisitFallback(type); + + TResult Visit(Fp64 type) => VisitFallback(type); + + TResult Visit(String type) => VisitFallback(type); + + TResult Visit(Binary type) => VisitFallback(type); + + TResult Visit(Date type) => VisitFallback(type); + + TResult Visit(IntervalYear type) => VisitFallback(type); + + TResult Visit(IntervalDay type) => VisitFallback(type); + + TResult Visit(IntervalCompound type) => VisitFallback(type); + + TResult Visit(Uuid type) => VisitFallback(type); + + TResult Visit(FixedChar type) => VisitFallback(type); + + TResult Visit(VarChar type) => VisitFallback(type); + + TResult Visit(FixedBinary type) => VisitFallback(type); + + TResult Visit(Decimal type) => VisitFallback(type); + + TResult Visit(PrecisionTime type) => VisitFallback(type); + + TResult Visit(PrecisionTimestamp type) => VisitFallback(type); + + TResult Visit(PrecisionTimestampTz type) => VisitFallback(type); + + TResult Visit(Func type) => VisitFallback(type); + + TResult Visit(Struct type) => VisitFallback(type); + + TResult Visit(List type) => VisitFallback(type); + + TResult Visit(Map type) => VisitFallback(type); +} diff --git a/src/Substrait.Core/Types/Simple/Binary.cs b/src/Substrait.Core/Types/Simple/Binary.cs new file mode 100644 index 0000000..d7258b8 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/Binary.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record Binary : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/Bool.cs b/src/Substrait.Core/Types/Simple/Bool.cs new file mode 100644 index 0000000..ca06fd7 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/Bool.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record Bool : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/Date.cs b/src/Substrait.Core/Types/Simple/Date.cs new file mode 100644 index 0000000..8183b94 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/Date.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record Date : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/Fp32.cs b/src/Substrait.Core/Types/Simple/Fp32.cs new file mode 100644 index 0000000..f530555 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/Fp32.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record Fp32 : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/Fp64.cs b/src/Substrait.Core/Types/Simple/Fp64.cs new file mode 100644 index 0000000..bfca396 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/Fp64.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record Fp64 : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/I16.cs b/src/Substrait.Core/Types/Simple/I16.cs new file mode 100644 index 0000000..3aefb36 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/I16.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record I16 : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/I32.cs b/src/Substrait.Core/Types/Simple/I32.cs new file mode 100644 index 0000000..0801a38 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/I32.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record I32 : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/I64.cs b/src/Substrait.Core/Types/Simple/I64.cs new file mode 100644 index 0000000..0b9d5c0 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/I64.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record I64 : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/I8.cs b/src/Substrait.Core/Types/Simple/I8.cs new file mode 100644 index 0000000..f44d86f --- /dev/null +++ b/src/Substrait.Core/Types/Simple/I8.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record I8 : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/IntervalYear.cs b/src/Substrait.Core/Types/Simple/IntervalYear.cs new file mode 100644 index 0000000..a6d9eec --- /dev/null +++ b/src/Substrait.Core/Types/Simple/IntervalYear.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record IntervalYear : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/String.cs b/src/Substrait.Core/Types/Simple/String.cs new file mode 100644 index 0000000..25f0c9a --- /dev/null +++ b/src/Substrait.Core/Types/Simple/String.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record String : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/Simple/Uuid.cs b/src/Substrait.Core/Types/Simple/Uuid.cs new file mode 100644 index 0000000..a011f76 --- /dev/null +++ b/src/Substrait.Core/Types/Simple/Uuid.cs @@ -0,0 +1,6 @@ +namespace Substrait.Core.Types.Simple; + +public sealed record Uuid : TypeClass +{ + public override TResult Accept(ITypeVisitor visitor) => visitor.Visit(this); +} diff --git a/src/Substrait.Core/Types/TypeClass.cs b/src/Substrait.Core/Types/TypeClass.cs new file mode 100644 index 0000000..085d5e5 --- /dev/null +++ b/src/Substrait.Core/Types/TypeClass.cs @@ -0,0 +1,8 @@ +namespace Substrait.Core.Types; + +public abstract record TypeClass +{ + public required bool Nullable { get; init; } + + public abstract TResult Accept(ITypeVisitor visitor); +} diff --git a/src/Substrait.Core/Types/TypeCreator.cs b/src/Substrait.Core/Types/TypeCreator.cs new file mode 100644 index 0000000..2585c80 --- /dev/null +++ b/src/Substrait.Core/Types/TypeCreator.cs @@ -0,0 +1,118 @@ +using Substrait.Core.Types.Compound; +using Substrait.Core.Types.Simple; +using Decimal = Substrait.Core.Types.Compound.Decimal; +using String = Substrait.Core.Types.Simple.String; + +namespace Substrait.Core.Types; + +public sealed class TypeCreator +{ + public static readonly TypeCreator Required = new(nullable: false); + + public static readonly TypeCreator Nullable = new(nullable: true); + + private readonly bool _nullable; + + private TypeCreator(bool nullable) + { + _nullable = nullable; + } + + public Bool Bool => new() { Nullable = _nullable }; + + public I8 I8 => new() { Nullable = _nullable }; + + public I16 I16 => new() { Nullable = _nullable }; + + public I32 I32 => new() { Nullable = _nullable }; + + public I64 I64 => new() { Nullable = _nullable }; + + public Fp32 Fp32 => new() { Nullable = _nullable }; + + public Fp64 Fp64 => new() { Nullable = _nullable }; + + public String String => new() { Nullable = _nullable }; + + public Binary Binary => new() { Nullable = _nullable }; + + public Date Date => new() { Nullable = _nullable }; + + public IntervalYear IntervalYear => new() { Nullable = _nullable }; + + public Uuid Uuid => new() { Nullable = _nullable }; + + public FixedChar FixedChar(int length) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + return new FixedChar { Nullable = _nullable, Length = length }; + } + + public VarChar VarChar(int length) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + return new VarChar { Nullable = _nullable, Length = length }; + } + + public FixedBinary FixedBinary(int length) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + return new FixedBinary { Nullable = _nullable, Length = length }; + } + + public Decimal Decimal(int precision, int scale) + { + ArgumentOutOfRangeException.ThrowIfNegative(precision); + ArgumentOutOfRangeException.ThrowIfGreaterThan(precision, MaxDecimalPrecision); + ArgumentOutOfRangeException.ThrowIfNegative(scale); + ArgumentOutOfRangeException.ThrowIfGreaterThan(scale, precision); + return new Decimal { Nullable = _nullable, Precision = precision, Scale = scale }; + } + + public PrecisionTime PrecisionTime(int precision) => + new() { Nullable = _nullable, Precision = SubsecondPrecision(precision) }; + + public PrecisionTimestamp PrecisionTimestamp(int precision) => + new() { Nullable = _nullable, Precision = SubsecondPrecision(precision) }; + + public PrecisionTimestampTz PrecisionTimestampTz(int precision) => + new() { Nullable = _nullable, Precision = SubsecondPrecision(precision) }; + + public IntervalDay IntervalDay(int precision) => + new() { Nullable = _nullable, Precision = SubsecondPrecision(precision) }; + + public IntervalCompound IntervalCompound(int precision) => + new() { Nullable = _nullable, Precision = SubsecondPrecision(precision) }; + + private const int MaxDecimalPrecision = 38; + + private const int MaxSubsecondPrecision = 12; + + private static int SubsecondPrecision(int precision) + { + ArgumentOutOfRangeException.ThrowIfNegative(precision); + ArgumentOutOfRangeException.ThrowIfGreaterThan(precision, MaxSubsecondPrecision); + return precision; + } + + public Func Func(IReadOnlyList parameterTypes, TypeClass returnType) => + new() { Nullable = _nullable, ParameterTypes = parameterTypes, ReturnType = returnType }; + + public Struct Struct(params TypeClass[] fields) => + new() { Nullable = _nullable, Fields = fields }; + + public Struct Struct(IEnumerable fields) => + new() { Nullable = _nullable, Fields = fields.ToArray() }; + + public List List(TypeClass elementType) => + new() { Nullable = _nullable, ElementType = elementType }; + + public Map Map(TypeClass key, TypeClass value) => + new() { Nullable = _nullable, Key = key, Value = value }; + + public static TypeCreator Of(bool nullable) => nullable ? Nullable : Required; + + public static TypeClass AsNullable(TypeClass type) => type with { Nullable = true }; + + public static TypeClass AsRequired(TypeClass type) => type with { Nullable = false }; +} diff --git a/substrait b/substrait index d9b9672..49e37e3 160000 --- a/substrait +++ b/substrait @@ -1 +1 @@ -Subproject commit d9b9672fd3c24285afdee9344fc2f4f7fcd70afb +Subproject commit 49e37e34312d1c133ecafa6e5df3cd8353a89632 diff --git a/substrait-csharp.sln b/substrait-csharp.sln deleted file mode 100644 index 7efe320..0000000 --- a/substrait-csharp.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.3.33027.108 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Substrait.Core", "src\Substrait.Core\Substrait.Core.csproj", "{C33A4305-4A80-4A92-AEF5-FB304D8BA6C7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C33A4305-4A80-4A92-AEF5-FB304D8BA6C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C33A4305-4A80-4A92-AEF5-FB304D8BA6C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C33A4305-4A80-4A92-AEF5-FB304D8BA6C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C33A4305-4A80-4A92-AEF5-FB304D8BA6C7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {671E873C-513C-474D-AA51-3532134EC36E} - EndGlobalSection -EndGlobal diff --git a/substrait-csharp.slnx b/substrait-csharp.slnx new file mode 100644 index 0000000..0707f75 --- /dev/null +++ b/substrait-csharp.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/Substrait.Core.Tests/Substrait.Core.Tests.csproj b/test/Substrait.Core.Tests/Substrait.Core.Tests.csproj new file mode 100644 index 0000000..0a696fa --- /dev/null +++ b/test/Substrait.Core.Tests/Substrait.Core.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + Exe + enable + enable + false + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Substrait.Core.Tests/SubstraitRelVisitorTests.cs b/test/Substrait.Core.Tests/SubstraitRelVisitorTests.cs new file mode 100644 index 0000000..36f7dfc --- /dev/null +++ b/test/Substrait.Core.Tests/SubstraitRelVisitorTests.cs @@ -0,0 +1,26 @@ +using Substrait.Core.Relation; + +namespace Substrait.Core.Tests; + +public class SubstraitRelVisitorTests +{ + private sealed class NoopVisitor : SubstraitRelVisitor + { + } + + [Fact] + public void Visit_WithNullRelation_ThrowsArgumentNullException() + { + var visitor = new NoopVisitor(); + + Assert.Throws(() => visitor.Visit((Read)null!)); + } + + [Fact] + public void Visit_WithUnhandledRelation_FallsBackAndThrows() + { + var visitor = new NoopVisitor(); + + Assert.Throws(() => visitor.Visit(new Read())); + } +} diff --git a/test/Substrait.Core.Tests/Type/StructuralEqualityTests.cs b/test/Substrait.Core.Tests/Type/StructuralEqualityTests.cs new file mode 100644 index 0000000..56724dc --- /dev/null +++ b/test/Substrait.Core.Tests/Type/StructuralEqualityTests.cs @@ -0,0 +1,126 @@ +using Substrait.Core.Types; +using Substrait.Core.Types.Compound; + +namespace Substrait.Core.Tests.Type; + +public class StructuralEqualityTests +{ + [Fact] + public void Struct_WithEqualFieldsFromDifferentLists_AreEqualAndHashesEqual() + { + var first = new Struct + { + Nullable = false, + Fields = new List { TypeCreator.Required.I32, TypeCreator.Required.String }, + }; + var second = new Struct + { + Nullable = false, + Fields = [TypeCreator.Required.I32, TypeCreator.Required.String], + }; + + Assert.Equal(first, second); + Assert.Equal(first.GetHashCode(), second.GetHashCode()); + } + + [Fact] + public void Struct_WithDifferentFields_AreNotEqual() + { + var first = TypeCreator.Required.Struct(TypeCreator.Required.I32); + var second = TypeCreator.Required.Struct(TypeCreator.Required.I64); + + Assert.NotEqual(first, second); + } + + [Fact] + public void Func_WithEqualParameterTypesFromDifferentLists_AreEqualAndHashesEqual() + { + var first = new Func + { + Nullable = false, + ParameterTypes = new List { TypeCreator.Required.I32 }, + ReturnType = TypeCreator.Required.Bool, + }; + var second = new Func + { + Nullable = false, + ParameterTypes = [TypeCreator.Required.I32], + ReturnType = TypeCreator.Required.Bool, + }; + + Assert.Equal(first, second); + Assert.Equal(first.GetHashCode(), second.GetHashCode()); + } + + [Fact] + public void Struct_WithReorderedFields_IsNotEqual() + { + var first = TypeCreator.Required.Struct(TypeCreator.Required.I32, TypeCreator.Required.I64); + var second = TypeCreator.Required.Struct(TypeCreator.Required.I64, TypeCreator.Required.I32); + + Assert.NotEqual(first, second); + } + + [Fact] + public void Struct_DifferingOnlyInNullability_IsNotEqual() + { + var required = TypeCreator.Required.Struct(TypeCreator.Required.I32); + var nullable = TypeCreator.Nullable.Struct(TypeCreator.Required.I32); + + Assert.NotEqual(required, nullable); + } + + [Fact] + public void Struct_IsNotMutatedByCallerOwnedArray() + { + var fields = new TypeClass[] { TypeCreator.Required.I32 }; + var type = TypeCreator.Required.Struct(fields); + var hashBefore = type.GetHashCode(); + + fields[0] = TypeCreator.Required.I64; + + Assert.Equal(hashBefore, type.GetHashCode()); + } + + [Fact] + public void Func_WithDifferentReturnType_IsNotEqual() + { + var first = new Func + { + Nullable = false, + ParameterTypes = [TypeCreator.Required.I32], + ReturnType = TypeCreator.Required.Bool, + }; + var second = new Func + { + Nullable = false, + ParameterTypes = [TypeCreator.Required.I32], + ReturnType = TypeCreator.Required.I64, + }; + + Assert.NotEqual(first, second); + } + + [Fact] + public void NestedTypes_CompareStructurally() + { + var first = TypeCreator.Required.Map( + TypeCreator.Required.String, + TypeCreator.Nullable.List(TypeCreator.Required.I32)); + var second = TypeCreator.Required.Map( + TypeCreator.Required.String, + TypeCreator.Nullable.List(TypeCreator.Required.I32)); + + Assert.Equal(first, second); + Assert.Equal(first.GetHashCode(), second.GetHashCode()); + } + + [Fact] + public void DifferentKindsWithIdenticalShape_AreNotEqual() + { + TypeClass first = TypeCreator.Required.PrecisionTime(6); + TypeClass second = TypeCreator.Required.PrecisionTimestamp(6); + + Assert.NotEqual(first, second); + } +} diff --git a/test/Substrait.Core.Tests/Type/TypeCreatorTests.cs b/test/Substrait.Core.Tests/Type/TypeCreatorTests.cs new file mode 100644 index 0000000..e94c422 --- /dev/null +++ b/test/Substrait.Core.Tests/Type/TypeCreatorTests.cs @@ -0,0 +1,96 @@ +using Substrait.Core.Types; +using Substrait.Core.Types.Compound; +using Decimal = Substrait.Core.Types.Compound.Decimal; + +namespace Substrait.Core.Tests.Type; + +public class TypeCreatorTests +{ + [Fact] + public void Required_ProducesNonNullableTypes() + { + Assert.False(TypeCreator.Required.Bool.Nullable); + Assert.False(TypeCreator.Required.FixedChar(10).Nullable); + } + + [Fact] + public void Nullable_ProducesNullableTypes() + { + Assert.True(TypeCreator.Nullable.Bool.Nullable); + Assert.True(TypeCreator.Nullable.FixedChar(10).Nullable); + } + + [Fact] + public void Of_ReturnsTheMatchingSingleton() + { + Assert.Same(TypeCreator.Required, TypeCreator.Of(false)); + Assert.Same(TypeCreator.Nullable, TypeCreator.Of(true)); + } + + [Fact] + public void FixedChar_SetsLength() + { + var type = Assert.IsType(TypeCreator.Required.FixedChar(10)); + + Assert.Equal(10, type.Length); + } + + [Fact] + public void Decimal_SetsPrecisionAndScale() + { + var type = Assert.IsType(TypeCreator.Required.Decimal(precision: 38, scale: 4)); + + Assert.Equal(38, type.Precision); + Assert.Equal(4, type.Scale); + } + + [Fact] + public void Struct_FromParams_BuildsFieldList() + { + var type = TypeCreator.Required.Struct(TypeCreator.Required.I32, TypeCreator.Nullable.String); + + Assert.Equal([TypeCreator.Required.I32, TypeCreator.Nullable.String], type.Fields); + } + + [Fact] + public void List_SetsElementType() + { + var type = TypeCreator.Nullable.List(TypeCreator.Required.I64); + + Assert.Equal(TypeCreator.Required.I64, type.ElementType); + Assert.True(type.Nullable); + } + + [Fact] + public void Map_SetsKeyAndValueTypes() + { + var type = TypeCreator.Required.Map(TypeCreator.Required.String, TypeCreator.Nullable.I32); + + Assert.Equal(TypeCreator.Required.String, type.Key); + Assert.Equal(TypeCreator.Nullable.I32, type.Value); + } + + [Fact] + public void AsNullable_TogglesNullabilityAndPreservesRuntimeType() + { + TypeClass required = TypeCreator.Required.FixedChar(10); + + var nullable = TypeCreator.AsNullable(required); + + var fixedChar = Assert.IsType(nullable); + Assert.True(fixedChar.Nullable); + Assert.Equal(10, fixedChar.Length); + } + + [Fact] + public void AsRequired_TogglesNullabilityAndPreservesRuntimeType() + { + TypeClass nullable = TypeCreator.Nullable.FixedChar(10); + + var required = TypeCreator.AsRequired(nullable); + + var fixedChar = Assert.IsType(required); + Assert.False(fixedChar.Nullable); + Assert.Equal(10, fixedChar.Length); + } +} diff --git a/test/Substrait.Core.Tests/Type/TypeVisitorTests.cs b/test/Substrait.Core.Tests/Type/TypeVisitorTests.cs new file mode 100644 index 0000000..c666517 --- /dev/null +++ b/test/Substrait.Core.Tests/Type/TypeVisitorTests.cs @@ -0,0 +1,120 @@ +using Substrait.Core.Types; +using Substrait.Core.Types.Compound; +using Substrait.Core.Types.Simple; +using Decimal = Substrait.Core.Types.Compound.Decimal; +using String = Substrait.Core.Types.Simple.String; + +namespace Substrait.Core.Tests.Type; + +public class TypeVisitorTests +{ + private sealed class KindNameVisitor : ITypeVisitor + { + public string Visit(Bool type) => nameof(Bool); + + public string Visit(I8 type) => nameof(I8); + + public string Visit(I16 type) => nameof(I16); + + public string Visit(I32 type) => nameof(I32); + + public string Visit(I64 type) => nameof(I64); + + public string Visit(Fp32 type) => nameof(Fp32); + + public string Visit(Fp64 type) => nameof(Fp64); + + public string Visit(String type) => nameof(String); + + public string Visit(Binary type) => nameof(Binary); + + public string Visit(Date type) => nameof(Date); + + public string Visit(IntervalYear type) => nameof(IntervalYear); + + public string Visit(IntervalDay type) => nameof(IntervalDay); + + public string Visit(IntervalCompound type) => nameof(IntervalCompound); + + public string Visit(Uuid type) => nameof(Uuid); + + public string Visit(FixedChar type) => nameof(FixedChar); + + public string Visit(VarChar type) => nameof(VarChar); + + public string Visit(FixedBinary type) => nameof(FixedBinary); + + public string Visit(Decimal type) => nameof(Decimal); + + public string Visit(PrecisionTime type) => nameof(PrecisionTime); + + public string Visit(PrecisionTimestamp type) => nameof(PrecisionTimestamp); + + public string Visit(PrecisionTimestampTz type) => nameof(PrecisionTimestampTz); + + public string Visit(Func type) => nameof(Func); + + public string Visit(Struct type) => nameof(Struct); + + public string Visit(List type) => nameof(List); + + public string Visit(Map type) => nameof(Map); + } + + public static TheoryData TypesAndExpectedKinds() => + new() + { + { TypeCreator.Required.Bool, nameof(Bool) }, + { TypeCreator.Required.I32, nameof(I32) }, + { TypeCreator.Required.String, nameof(String) }, + { TypeCreator.Required.FixedChar(5), nameof(FixedChar) }, + { TypeCreator.Required.Decimal(10, 2), nameof(Decimal) }, + { TypeCreator.Required.Struct(TypeCreator.Required.Bool), nameof(Struct) }, + { TypeCreator.Required.List(TypeCreator.Required.Bool), nameof(List) }, + { TypeCreator.Required.Map(TypeCreator.Required.String, TypeCreator.Required.I32), nameof(Map) }, + }; + + [Theory] + [MemberData(nameof(TypesAndExpectedKinds))] + public void Accept_DispatchesToMatchingVisitMethod(TypeClass type, string expectedKind) + { + var visitor = new KindNameVisitor(); + + var result = type.Accept(visitor); + + Assert.Equal(expectedKind, result); + } + + [Fact] + public void EveryTypeClass_HasAVisitorOverload() + { + var kinds = typeof(TypeClass) + .Assembly.GetTypes() + .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(TypeClass))) + .ToList(); + + var covered = typeof(ITypeVisitor<>) + .GetMethods() + .Where(m => m.Name == "Visit") + .Select(m => m.GetParameters()[0].ParameterType) + .ToHashSet(); + + var missing = kinds.Where(k => !covered.Contains(k)).Select(k => k.Name).Order().ToList(); + + Assert.True(missing.Count == 0, $"No ITypeVisitor overload for: {string.Join(", ", missing)}"); + } + + [Fact] + public void Accept_DispatchesOnTheRuntimeKind() + { + var visitor = new FallbackOnlyVisitor(); + + Assert.Equal(nameof(Decimal), TypeCreator.Required.Decimal(10, 2).Accept(visitor)); + Assert.Equal(nameof(List), TypeCreator.Required.List(TypeCreator.Required.I32).Accept(visitor)); + } + + private sealed class FallbackOnlyVisitor : ITypeVisitor + { + public string VisitFallback(TypeClass type) => type.GetType().Name; + } +}