Skip to content

Commit 3329339

Browse files
committed
SQL-specific type information now follows values everywhere.
1 parent 01f71d1 commit 3329339

11 files changed

Lines changed: 343 additions & 118 deletions

File tree

SqlServerSimulator/DataType.cs

Lines changed: 210 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,33 @@ namespace SqlServerSimulator;
77
/// <summary>
88
/// Bridges .NET's native types, the various <see cref="DbType"/>s, and SQL Server's actual behavior.
99
/// </summary>
10-
internal abstract class DataType
10+
internal abstract class DataType : IComparer<DataValue>, IComparable<DataType>
1111
{
1212
private protected DataType()
1313
{
1414
}
1515

1616
public abstract DbType Type { get; }
1717

18-
public abstract object ConvertFrom(object value);
18+
/// <summary>
19+
/// Precedence according to the ranking at https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-type-precedence-transact-sql
20+
/// </summary>
21+
protected abstract int Precedence { get; }
22+
23+
public int CompareTo(DataType? other)
24+
{
25+
ArgumentNullException.ThrowIfNull(other);
26+
27+
return this == other ? 0 : this.CompareTo(other);
28+
}
29+
30+
public DataValue ConvertFrom(object? value) => ConvertFrom(new DataValue(value, this));
31+
32+
public abstract DataValue ConvertFrom(DataValue value);
33+
34+
public abstract int Compare(DataValue x, DataValue y);
35+
36+
public abstract int DataLength(DataValue value);
1937

2038
public override string ToString() => Type.ToString();
2139

@@ -33,6 +51,32 @@ private protected DataType()
3351

3452
public static readonly DataType BuiltInDbSystemName = new DbSystemName();
3553

54+
public static NumericCompatibleDataType CommonNumeric(DataValue a, DataValue b, char op)
55+
{
56+
var (at, bt) = (a.Type, b.Type);
57+
return at is not NumericCompatibleDataType atb || bt is not NumericCompatibleDataType btb
58+
? throw SimulatedSqlException.DataTypeIncompatible(at, bt, op)
59+
: at.CompareTo(bt) switch
60+
{
61+
< 0 => atb,
62+
0 => atb,
63+
> 0 => btb,
64+
};
65+
}
66+
67+
public static BitwiseCompatibleDataType CommonInteger(DataValue a, DataValue b, char op)
68+
{
69+
var (at, bt) = (a.Type, b.Type);
70+
return at is not BitwiseCompatibleDataType atb || bt is not BitwiseCompatibleDataType btb
71+
? throw SimulatedSqlException.DataTypeIncompatible(at, bt, op)
72+
: at.CompareTo(bt) switch
73+
{
74+
< 0 => atb,
75+
0 => atb,
76+
> 0 => btb,
77+
};
78+
}
79+
3680
/// <summary>
3781
/// Looks up the <see cref="DataType"/> for the provided type name.
3882
/// </summary>
@@ -76,46 +120,200 @@ public static DataType GetByName(Name name, int index)
76120
_ => throw new NotSupportedException($"Simulated data type parser doesn't recognize DbType {dbType}"),
77121
};
78122

79-
private sealed class DbBoolean : DataType
123+
public abstract class NumericCompatibleDataType : DataType
124+
{
125+
protected abstract int DataLength();
126+
127+
public sealed override int DataLength(DataValue value) => DataLength();
128+
129+
public abstract object? Add(object? a, object? b);
130+
131+
public DataValue Add(DataValue a, DataValue b) => new(Add(a.Value, b.Value), this);
132+
133+
public abstract object? Subtract(object? a, object? b);
134+
135+
public DataValue Subtract(DataValue a, DataValue b) => new(Subtract(a.Value, b.Value), this);
136+
137+
public abstract object? Multiply(object? a, object? b);
138+
139+
public DataValue Multiply(DataValue a, DataValue b) => new(Multiply(a.Value, b.Value), this);
140+
141+
public abstract object? Divide(object? a, object? b);
142+
143+
public DataValue Divide(DataValue a, DataValue b) => new(Divide(a.Value, b.Value), this);
144+
}
145+
146+
public abstract class BitwiseCompatibleDataType : NumericCompatibleDataType
147+
{
148+
public abstract object? BitwiseAnd(object? a, object? b);
149+
150+
public DataValue BitwiseAnd(DataValue a, DataValue b) => new(BitwiseAnd(a.Value, b.Value), this);
151+
152+
public abstract object? BitwiseOr(object? a, object? b);
153+
154+
public DataValue BitwiseOr(DataValue a, DataValue b) => new(BitwiseOr(a.Value, b.Value), this);
155+
156+
public abstract object? BitwiseExclusiveOr(object? a, object? b);
157+
158+
public DataValue BitwiseExclusiveOr(DataValue a, DataValue b) => new(BitwiseExclusiveOr(a.Value, b.Value), this);
159+
}
160+
161+
private sealed class DbBoolean : BitwiseCompatibleDataType
80162
{
81163
public override DbType Type => DbType.Boolean;
82164

83-
public override object ConvertFrom(object value) => Convert.ToBoolean(value, CultureInfo.InvariantCulture);
165+
protected override int Precedence => 20;
166+
167+
public override DataValue ConvertFrom(DataValue value) => new(Convert.ToBoolean(value, CultureInfo.InvariantCulture), this);
168+
169+
public override int Compare(DataValue x, DataValue y) => throw new NotImplementedException();
170+
171+
protected override int DataLength() => 1;
172+
173+
public override object? Add(object? a, object? b) => throw new NotImplementedException();
174+
175+
public override object? Subtract(object? a, object? b) => throw new NotImplementedException();
176+
177+
public override object? Multiply(object? a, object? b) => throw new NotImplementedException();
178+
179+
public override object? Divide(object? a, object? b) => throw new NotImplementedException();
180+
181+
public override object? BitwiseAnd(object? a, object? b) => throw new NotImplementedException();
182+
183+
public override object? BitwiseOr(object? a, object? b) => throw new NotImplementedException();
184+
185+
public override object? BitwiseExclusiveOr(object? a, object? b) => throw new NotImplementedException();
84186
}
85187

86-
private sealed class DbByte : DataType
188+
private sealed class DbByte : BitwiseCompatibleDataType
87189
{
88190
public override DbType Type => DbType.Byte;
89191

90-
public override object ConvertFrom(object value) => Convert.ToByte(value, CultureInfo.InvariantCulture);
192+
protected override int Precedence => 19;
193+
194+
public override DataValue ConvertFrom(DataValue value) => new(Convert.ToByte(value, CultureInfo.InvariantCulture), this);
195+
196+
public override int Compare(DataValue x, DataValue y) => throw new NotImplementedException();
197+
198+
protected override int DataLength() => 1;
199+
200+
public override object? Add(object? a, object? b) => throw new NotImplementedException();
201+
202+
public override object? Subtract(object? a, object? b) => throw new NotImplementedException();
203+
204+
public override object? Multiply(object? a, object? b) => throw new NotImplementedException();
205+
206+
public override object? Divide(object? a, object? b) => throw new NotImplementedException();
207+
208+
public override object? BitwiseAnd(object? a, object? b) => throw new NotImplementedException();
209+
210+
public override object? BitwiseOr(object? a, object? b) => throw new NotImplementedException();
211+
212+
public override object? BitwiseExclusiveOr(object? a, object? b) => throw new NotImplementedException();
91213
}
92214

93-
private sealed class DbInt16 : DataType
215+
private sealed class DbInt16 : BitwiseCompatibleDataType
94216
{
95217
public override DbType Type => DbType.Int16;
96218

97-
public override object ConvertFrom(object value) => Convert.ToInt16(value, CultureInfo.InvariantCulture);
219+
protected override int Precedence => 18;
220+
221+
public override DataValue ConvertFrom(DataValue value) => new(Convert.ToInt16(value, CultureInfo.InvariantCulture), this);
222+
223+
public override int Compare(DataValue x, DataValue y) => throw new NotImplementedException();
224+
225+
protected override int DataLength() => 2;
226+
227+
public override object? Add(object? a, object? b) => throw new NotImplementedException();
228+
229+
public override object? Subtract(object? a, object? b) => throw new NotImplementedException();
230+
231+
public override object? Multiply(object? a, object? b) => throw new NotImplementedException();
232+
233+
public override object? Divide(object? a, object? b) => throw new NotImplementedException();
234+
235+
public override object? BitwiseAnd(object? a, object? b) => throw new NotImplementedException();
236+
237+
public override object? BitwiseOr(object? a, object? b) => throw new NotImplementedException();
238+
239+
public override object? BitwiseExclusiveOr(object? a, object? b) => throw new NotImplementedException();
98240
}
99241

100-
private sealed class DbInt32 : DataType
242+
private sealed class DbInt32 : BitwiseCompatibleDataType
101243
{
102244
public override DbType Type => DbType.Int32;
103245

104-
public override object ConvertFrom(object value) => Convert.ToInt32(value, CultureInfo.InvariantCulture);
246+
protected override int Precedence => 17;
247+
248+
public static int ToNative(object value) => Convert.ToInt32(value, CultureInfo.InvariantCulture);
249+
250+
public override DataValue ConvertFrom(DataValue value) => new(value.Value is null ? null : ToNative(value.Value), this);
251+
252+
public override int Compare(DataValue x, DataValue y)
253+
{
254+
var (xv, yv) = (x.Value, y.Value);
255+
if (xv is null)
256+
return yv is null ? 0 : -1;
257+
else if (yv is null)
258+
return 1;
259+
260+
return ToNative(x).CompareTo(ToNative(y));
261+
}
262+
263+
protected override int DataLength() => 4;
264+
265+
private static bool TryToNative(object? rawA, object? rawB, out int a, out int b)
266+
{
267+
if (rawA is null || rawB is null)
268+
{
269+
a = default;
270+
b = default;
271+
return false;
272+
}
273+
274+
a = ToNative(rawA);
275+
b = ToNative(rawB);
276+
return true;
277+
}
278+
279+
public override object? Add(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA + nativeB : null;
280+
281+
public override object? Subtract(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA - nativeB : null;
282+
283+
public override object? Multiply(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA * nativeB : null;
284+
285+
public override object? Divide(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA / nativeB : null;
286+
287+
public override object? BitwiseAnd(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA & nativeB : null;
288+
289+
public override object? BitwiseOr(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA | nativeB : null;
290+
291+
public override object? BitwiseExclusiveOr(object? a, object? b) => TryToNative(a, b, out var nativeA, out var nativeB) ? nativeA ^ nativeB : null;
105292
}
106293

107294
private sealed class DbAnsiString : DataType
108295
{
109296
public override DbType Type => DbType.AnsiString;
110297

111-
public override object ConvertFrom(object value) => value.ToString() ?? throw new InvalidOperationException("value's ToString method returned null.");
298+
protected override int Precedence => 28;
299+
300+
public override DataValue ConvertFrom(DataValue value) => new(value.Value?.ToString(), this);
301+
302+
public override int Compare(DataValue x, DataValue y) => throw new NotImplementedException();
303+
304+
public override int DataLength(DataValue value) => throw new NotImplementedException();
112305
}
113306

114307
private class DbString : DataType
115308
{
116309
public override DbType Type => DbType.String;
117310

118-
public override object ConvertFrom(object value) => value.ToString() ?? throw new InvalidOperationException("value's ToString method returned null.");
311+
protected override int Precedence => 26;
312+
313+
public override DataValue ConvertFrom(DataValue value) => new(value.Value?.ToString(), this);
314+
315+
public override int Compare(DataValue x, DataValue y) => throw new NotImplementedException();
316+
public override int DataLength(DataValue value) => throw new NotImplementedException();
119317
}
120318

121319
private sealed class DbSystemName : DbString

SqlServerSimulator/DataValue.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace SqlServerSimulator;
2+
3+
/// <summary>
4+
/// Wraps a raw value with type information.
5+
/// <see cref="object"/> can't be directly used because of differences in type handling between .NET and SQL Server.
6+
/// </summary>
7+
internal readonly struct DataValue : IComparable<DataValue>
8+
{
9+
/// <summary>
10+
/// The wrapped value.
11+
/// </summary>
12+
public readonly object? Value;
13+
14+
/// <summary>
15+
/// The SQL data type of <see cref="Value"/>.
16+
/// </summary>
17+
/// <remarks>Unlike .NET, "null" is typed in SQL Server and strings have several additional properties.</remarks>
18+
public DataType Type => field ?? DataType.BuiltInDbInt32;
19+
20+
public DataValue(object? value, DataType type)
21+
{
22+
System.Diagnostics.Debug.Assert(value is not DataValue);
23+
24+
this.Value = value;
25+
this.Type = type;
26+
}
27+
28+
public DataValue(int value) : this(value, DataType.BuiltInDbInt32)
29+
{
30+
}
31+
32+
public int CompareTo(DataValue other) => Type.Compare(this, other);
33+
34+
#if DEBUG
35+
public override string ToString() => $"({Type}) {Value ?? "NULL"}";
36+
#endif
37+
}

SqlServerSimulator/Parser/BooleanExpression.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ public static BooleanExpression Parse(ParserContext context)
3232
/// </summary>
3333
/// <param name="getColumnValue">Provides the value for a column.</param>
3434
/// <returns>The result of the expression.</returns>
35-
public abstract bool Run(Func<List<string>, object?> getColumnValue);
35+
public abstract bool Run(Func<List<string>, DataValue> getColumnValue);
3636

3737
#if DEBUG
3838
public abstract override string ToString();
3939
#endif
4040

4141
private sealed class EqualityExpression(Expression left, Expression right) : BooleanExpression
4242
{
43-
public override bool Run(Func<List<string>, object?> getColumnValue) =>
44-
(left.Run(getColumnValue)?.Equals(right.Run(getColumnValue))).GetValueOrDefault();
43+
public override bool Run(Func<List<string>, DataValue> getColumnValue) =>
44+
(left.Run(getColumnValue).Value?.Equals(right.Run(getColumnValue).Value)).GetValueOrDefault();
4545

4646
#if DEBUG
4747
public override string ToString() => $"{left} = {right}";

0 commit comments

Comments
 (0)