Skip to content

Commit 2b042d6

Browse files
committed
Added support for the DATALENGTH built-in function.
1 parent 845bf05 commit 2b042d6

8 files changed

Lines changed: 128 additions & 28 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
[TestClass]
7+
public sealed class BuiltInFunctionTests
8+
{
9+
private static T ExecuteScalar<T>(string commandText) where T : struct => new Simulation().ExecuteScalar<T>(commandText);
10+
11+
[TestMethod]
12+
public void UnrecognizedBuiltInFunction()
13+
{
14+
var exception = Throws<DbException>(() => ExecuteScalar<int>("select frog()"));
15+
AreEqual("'frog' is not a recognized built-in function name.", exception.Message);
16+
}
17+
18+
[TestMethod]
19+
public void DataLength() => AreEqual(4, ExecuteScalar<int>("select datalength(1)"));
20+
21+
[TestMethod]
22+
public void DataLengthAllCaps() => AreEqual(4, ExecuteScalar<int>("select DATALENGTH(1)"));
23+
}

SqlServerSimulator.Tests/Extensions.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,28 @@ public static DbConnection CreateOpenConnection(this Simulation simulation)
3939
}
4040

4141
public static int ExecuteNonQuery(this Simulation simulation, string commandText)
42-
=> simulation.CreateCommand(commandText).ExecuteNonQuery();
42+
{
43+
using var connection = simulation.CreateOpenConnection();
44+
using var command = connection.CreateCommand(commandText);
45+
return command.ExecuteNonQuery();
46+
}
4347

4448
public static object? ExecuteScalar(this Simulation simulation, string commandText)
45-
=> simulation.CreateCommand(commandText).ExecuteScalar();
49+
{
50+
using var connection = simulation.CreateOpenConnection();
51+
using var command = connection.CreateCommand(commandText);
52+
return command.ExecuteScalar();
53+
}
54+
55+
public static T ExecuteScalar<T>(this Simulation simulation, string commandText)
56+
where T : struct
57+
{
58+
using var connection = simulation.CreateOpenConnection();
59+
using var command = connection.CreateCommand(commandText);
60+
var result = command.ExecuteScalar();
61+
Assert.IsNotNull(result);
62+
return Assert.IsInstanceOfType<T>(result);
63+
}
4664

4765
public static void ValidateSyntaxError(this Simulation simulation, string commandText, string nearSyntax)
4866
{

SqlServerSimulator/Parser/Expression.cs

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using SqlServerSimulator.Parser.Tokens;
2+
using System.Collections.Frozen;
23

34
namespace SqlServerSimulator.Parser;
45

@@ -69,16 +70,30 @@ public static Expression Parse(Simulation simulation, IEnumerator<Token> tokens,
6970
if (expression is null)
7071
throw new NotSupportedException("Simulated expression parser doesn't know how to handle '.' at the start of an expression.");
7172

72-
if (expression is not Reference reference)
73-
throw new NotSupportedException("Simulated expression parser doesn't know how to handle '.' here.");
73+
{
74+
if (expression is not Reference reference)
75+
throw new NotSupportedException("Simulated expression parser doesn't know how to handle '.' here.");
7476

75-
reference.AddMultiPartComponent(tokens.RequireNext<Name>());
77+
reference.AddMultiPartComponent(tokens.RequireNext<Name>());
78+
}
7679
break;
7780
case Comma:
7881
case CloseParentheses:
7982
if (expression is null)
8083
throw SimulatedSqlException.SyntaxErrorNear(token);
8184
return expression;
85+
case OpenParentheses:
86+
{
87+
if (expression is not Reference reference)
88+
throw SimulatedSqlException.SyntaxErrorNear(token);
89+
if (!BuiltInFunctions.TryGetValue(reference.Name, out var builtInFunction))
90+
throw SimulatedSqlException.UnrecognizedBuiltInFunction(reference.Name);
91+
92+
token = tokens.RequireNext(); // Move past (
93+
expression = builtInFunction(simulation, tokens, ref token, getVariableValue);
94+
_ = tokens.TryMoveNext(out token); // Move past )
95+
return expression;
96+
}
8297
default:
8398
throw new NotSupportedException($"Simulated expression parser doesn't know how to handle '{token}'.");
8499
}
@@ -93,6 +108,12 @@ public static Expression Parse(Simulation simulation, IEnumerator<Token> tokens,
93108
public abstract override string ToString();
94109
#endif
95110

111+
private delegate Expression FunctionResolver(Simulation simulation, IEnumerator<Token> tokens, ref Token? token, Func<string, object?> getVariableValue);
112+
113+
private static readonly FrozenDictionary<string, FunctionResolver> BuiltInFunctions = FrozenDictionary.Create<string, FunctionResolver>(Collation.Default, [
114+
new("datalength", (simulation, tokens, ref token, getVariableValue) => new DataLength(Expression.Parse(simulation, tokens, ref token, getVariableValue)))
115+
]);
116+
96117
private sealed class NamedExpression(Expression expression, string name) : Expression
97118
{
98119
private readonly Expression expression = expression;
@@ -179,29 +200,34 @@ public sealed class Add(Expression left, Expression right) : Expression
179200
#endif
180201
}
181202

182-
public sealed class Reference : Expression
203+
public sealed class Reference(Name name) : Expression
183204
{
184-
private readonly List<string> name = [];
185-
186-
public Reference(Name name)
187-
{
188-
this.name.Add(name.Value);
189-
}
205+
private readonly List<string> name = [name.Value];
190206

191207
public override string Name => this.name.Last();
192208

193-
public void AddMultiPartComponent(Name name)
194-
{
195-
this.name.Add(name.Value);
196-
}
209+
public void AddMultiPartComponent(Name name) => this.name.Add(name.Value);
197210

198-
public override object? Run(Func<List<string>, object?> getColumnValue)
199-
{
200-
return getColumnValue(this.name);
201-
}
211+
public override object? Run(Func<List<string>, object?> getColumnValue) => getColumnValue(this.name);
202212

203213
#if DEBUG
204214
public override string ToString() => string.Join('.', name);
215+
#endif
216+
}
217+
218+
public sealed class DataLength(Expression source) : Expression
219+
{
220+
private readonly Expression source = source;
221+
222+
public override object? Run(Func<List<string>, object?> getColumnValue) => source.Run(getColumnValue) switch
223+
{
224+
null => null,
225+
int => 4,
226+
_ => throw new NotSupportedException($"Simulation unable to to run DATALENGTH function on the provided expression."),
227+
};
228+
229+
#if DEBUG
230+
public override string ToString() => $"DATALENGTH({source})";
205231
#endif
206232
}
207233
}

SqlServerSimulator/Parser/Tokenizer.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,19 @@ public static IEnumerable<Token> Tokenize(string? command)
9191

9292
case '(':
9393
if (state.IsQuotedString())
94+
{
9495
_ = buffer.Append(c);
96+
}
9597
else
98+
{
99+
if (buffer.Length != 0)
100+
{
101+
yield return new UnquotedString(buffer);
102+
state = State.None;
103+
}
104+
96105
yield return new OpenParentheses();
106+
}
97107
continue;
98108
case ')':
99109
if (state.IsQuotedString())
@@ -110,6 +120,9 @@ public static IEnumerable<Token> Tokenize(string? command)
110120
case State.AtPrefixedString:
111121
yield return new AtPrefixedString(buffer);
112122
break;
123+
case State.Numeric:
124+
yield return new Numeric(buffer);
125+
break;
113126
}
114127
yield return new CloseParentheses();
115128
state = State.None;
@@ -159,7 +172,12 @@ public static IEnumerable<Token> Tokenize(string? command)
159172
continue;
160173

161174
case '-':
162-
c = commandEnumerator.GetNext(ref index);
175+
if (!commandEnumerator.TryGetNext(out c, ref index))
176+
{
177+
yield return new Minus();
178+
continue;
179+
}
180+
163181
switch (c)
164182
{
165183
case '-':
@@ -177,6 +195,10 @@ public static IEnumerable<Token> Tokenize(string? command)
177195
}
178196
break;
179197

198+
case '*':
199+
yield return new Asterisk();
200+
continue;
201+
180202
case '.':
181203
switch (state)
182204
{
@@ -248,10 +270,6 @@ static bool TryGetNext(this CharEnumerator enumerator, out char c, ref int index
248270
return false;
249271
}
250272

251-
static char GetNext(this CharEnumerator enumerator, ref int index) => !enumerator.TryGetNext(out var c, ref index)
252-
? throw new SimulatedSqlException($"Simulated syntax error at index {index}.")
253-
: c;
254-
255273
static bool IsQuotedString(this State state) => state switch
256274
{
257275
State.BracketDelimitedString or State.SingleQuotedString or State.DoubleQuotedString => true,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace SqlServerSimulator.Parser.Tokens;
2+
3+
sealed class Asterisk : Token
4+
{
5+
public override string ToString() => "*";
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace SqlServerSimulator.Parser.Tokens;
2+
3+
sealed class Minus : Token
4+
{
5+
public override string ToString() => "-";
6+
}

SqlServerSimulator/SimulatedSqlException.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,6 @@ private SimulatedSqlException(string? message, params ReadOnlySpan<SimulatedSqlE
9393
internal static SimulatedSqlException SyntaxErrorNear(Token token) => new($"Incorrect syntax near '{token}'.", 102, 15, 1);
9494

9595
internal static SimulatedSqlException ThereIsAlreadyAnObject(string name) => new($"There is already an object named '{name}' in the database.", 2714, 16, 6);
96+
97+
internal static SimulatedSqlException UnrecognizedBuiltInFunction(string name) => new($"'{name}' is not a recognized built-in function name.", 195, 15, 10);
9698
}

SqlServerSimulator/Simulation.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,10 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
291291
continue;
292292
}
293293
break;
294-
}
295294

296-
throw new NotSupportedException($"Simulated command processor doesn't know what to do with {token}.");
297-
}
295+
default:
296+
throw SimulatedSqlException.SyntaxErrorNear(token);
297+
}
298+
} // while (tokens.TryMoveNext(out var token))
298299
}
299300
}

0 commit comments

Comments
 (0)