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;
+ }
+}