Skip to content

Commit 1fa4e6c

Browse files
committed
Reserved keywords are now tokenized separately from unquoted strings.
1 parent 0c8f991 commit 1fa4e6c

9 files changed

Lines changed: 104 additions & 53 deletions

File tree

SqlServerSimulator/Parser/Expression.cs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,28 @@ public static Expression Parse(Simulation simulation, IEnumerator<Token> tokens,
3131
case DoubleAtPrefixedString doubleAtPrefixedString:
3232
expression = new Value(doubleAtPrefixedString);
3333
break;
34-
case Name name:
35-
if (name is UnquotedString && name.TryParse(out var keyword))
34+
case ReservedKeyword reservedKeyword:
35+
switch (reservedKeyword.Keyword)
3636
{
37-
switch (keyword)
38-
{
39-
case Keyword.As:
40-
if (expression is null || !tokens.TryMoveNext(out token) || token is not Name alias)
41-
throw SimulatedSqlException.SyntaxErrorNear(name);
42-
43-
expression = new NamedExpression(expression, alias.Value);
44-
_ = tokens.TryMoveNext(out token);
45-
return expression;
46-
case Keyword.From:
47-
if (expression is null)
48-
throw SimulatedSqlException.SyntaxErrorNear(name);
49-
50-
return expression;
51-
case Keyword.Null:
52-
expression = new Value();
53-
continue;
54-
}
37+
case Keyword.As:
38+
if (expression is null || !tokens.TryMoveNext(out token) || token is not Name alias)
39+
throw SimulatedSqlException.SyntaxErrorNearKeyword(reservedKeyword);
40+
41+
expression = new NamedExpression(expression, alias.Value);
42+
_ = tokens.TryMoveNext(out token);
43+
return expression;
44+
case Keyword.From:
45+
if (expression is null)
46+
throw SimulatedSqlException.SyntaxErrorNearKeyword(reservedKeyword);
47+
48+
return expression;
49+
case Keyword.Null:
50+
expression = new Value();
51+
continue;
5552
}
5653

54+
throw SimulatedSqlException.SyntaxErrorNearKeyword(reservedKeyword);
55+
case Name name:
5756
expression = new Reference(name);
5857
break;
5958
case Plus:

SqlServerSimulator/Parser/Selection.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ public static Selection Parse(Simulation simulation, IEnumerator<Token> tokens,
3535
[[.. expressions.Select(x => x.Run(column => throw SimulatedSqlException.InvalidColumnName(column)))]]
3636
));
3737

38-
case UnquotedString unquotedString:
39-
if (!unquotedString.TryParse(out var keyword) || keyword != Keyword.From)
38+
case ReservedKeyword expectFrom:
39+
if (expectFrom.Keyword != Keyword.From)
4040
throw new NotSupportedException("Simulated selection processor expected a `from`.");
4141

4242
switch (token = tokens.RequireNext())
@@ -47,9 +47,9 @@ public static Selection Parse(Simulation simulation, IEnumerator<Token> tokens,
4747

4848
if (tokens.TryMoveNext(out token))
4949
{
50-
if (token is UnquotedString maybeAs && maybeAs.Parse() == Keyword.As)
50+
if (token is ReservedKeyword { Keyword: Keyword.As })
5151
{
52-
if (token is not UnquotedString)
52+
if (token is not ReservedKeyword)
5353
break;
5454
}
5555
else
@@ -68,15 +68,15 @@ public static Selection Parse(Simulation simulation, IEnumerator<Token> tokens,
6868
));
6969

7070
case OpenParentheses:
71-
if ((token = tokens.RequireNext()) is not UnquotedString maybeSelect || maybeSelect.Parse() != Keyword.Select)
71+
if ((token = tokens.RequireNext()) is not ReservedKeyword { Keyword: Keyword.Select })
7272
throw SimulatedSqlException.SyntaxErrorNear(token);
7373

7474
{
7575
var derived = Selection.Parse(simulation, tokens, ref token, getVariableValue, depth + 1).Results;
7676

77-
if ((token = tokens.RequireNext()) is UnquotedString maybeAs && maybeAs.Parse() == Keyword.As)
77+
if ((token = tokens.RequireNext()) is ReservedKeyword { Keyword: Keyword.As })
7878
{
79-
if (token is not UnquotedString)
79+
if (token is not ReservedKeyword)
8080
break;
8181
}
8282
else

SqlServerSimulator/Parser/Tokenizer.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public static IEnumerable<Token> Tokenize(string? command)
9999
{
100100
if (buffer.Length != 0)
101101
{
102-
yield return new UnquotedString(buffer);
102+
yield return UnquotedString.CheckReserved(buffer);
103103
state = State.None;
104104
}
105105

@@ -116,7 +116,7 @@ public static IEnumerable<Token> Tokenize(string? command)
116116
switch (state)
117117
{
118118
case State.UnquotedString:
119-
yield return new UnquotedString(buffer);
119+
yield return UnquotedString.CheckReserved(buffer);
120120
break;
121121
case State.AtPrefixedString:
122122
yield return new AtPrefixedString(buffer);
@@ -134,7 +134,7 @@ public static IEnumerable<Token> Tokenize(string? command)
134134
switch (state)
135135
{
136136
case State.UnquotedString:
137-
yield return new UnquotedString(buffer);
137+
yield return UnquotedString.CheckReserved(buffer);
138138
state = State.None;
139139
break;
140140
case State.Numeric:
@@ -152,7 +152,7 @@ public static IEnumerable<Token> Tokenize(string? command)
152152
switch (state)
153153
{
154154
case State.UnquotedString:
155-
yield return new UnquotedString(buffer);
155+
yield return UnquotedString.CheckReserved(buffer);
156156
state = State.None;
157157
break;
158158
case State.AtPrefixedString:
@@ -205,7 +205,7 @@ public static IEnumerable<Token> Tokenize(string? command)
205205
switch (state)
206206
{
207207
case State.UnquotedString:
208-
yield return new UnquotedString(buffer);
208+
yield return UnquotedString.CheckReserved(buffer);
209209
break;
210210
case State.AtPrefixedString:
211211
yield return new AtPrefixedString(buffer);
@@ -229,7 +229,7 @@ public static IEnumerable<Token> Tokenize(string? command)
229229
yield return new Numeric(buffer);
230230
break;
231231
case State.UnquotedString:
232-
yield return new UnquotedString(buffer);
232+
yield return UnquotedString.CheckReserved(buffer);
233233
break;
234234
}
235235

@@ -245,7 +245,7 @@ public static IEnumerable<Token> Tokenize(string? command)
245245
switch (state)
246246
{
247247
case State.UnquotedString:
248-
yield return new UnquotedString(buffer);
248+
yield return UnquotedString.CheckReserved(buffer);
249249
break;
250250
case State.AtPrefixedString:
251251
yield return new AtPrefixedString(buffer);

SqlServerSimulator/Parser/Tokens/Name.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ namespace SqlServerSimulator.Parser.Tokens;
44

55
abstract class Name : StringToken
66
{
7+
/// <summary>
8+
/// Creates a <see cref="Name"/> from a plain string.
9+
/// </summary>
10+
/// <param name="value">Transfers to <see cref="StringToken.Value"/>.</param>
11+
private protected Name(string value)
12+
: base(value)
13+
{
14+
}
15+
16+
/// <summary>
17+
/// Creates a <see cref="Name"/> from the provided <paramref name="buffer"/> and then <see cref="StringBuilder.Clear"/>s it.
18+
/// </summary>
19+
/// <param name="buffer">The source of the string.</param>
720
private protected Name(StringBuilder buffer)
821
: base(buffer)
922
{
1023
}
11-
12-
public bool TryParse(out Keyword keyword) => Enum.TryParse(Value, true, out keyword);
13-
14-
public Keyword Parse() => !Enum.TryParse<Keyword>(Value, true, out var result)
15-
? throw new NotSupportedException($"Simulated command processor doesn't know what to do with `{Value}`.")
16-
: result;
1724
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace SqlServerSimulator.Parser.Tokens;
2+
3+
internal sealed class ReservedKeyword(Keyword keyword, string value) : Token
4+
{
5+
public readonly Keyword Keyword = keyword;
6+
7+
/// <summary>
8+
/// The original value as provided in the command.
9+
/// </summary>
10+
/// <remarks>This preserves input casing for potential error messages.</remarks>
11+
public readonly string Value = value;
12+
13+
public override string ToString() => this.Value;
14+
}

SqlServerSimulator/Parser/Tokens/StringToken.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@ abstract class StringToken : Token
66
{
77
public readonly string Value;
88

9+
/// <summary>
10+
/// Creates a <see cref="StringToken"/> from a plain string.
11+
/// </summary>
12+
/// <param name="value">Transfers to <see cref="Value"/>.</param>
13+
private protected StringToken(string value) => this.Value = value;
14+
15+
/// <summary>
16+
/// Creates a <see cref="StringToken"/> from the provided <paramref name="buffer"/> and then <see cref="StringBuilder.Clear"/>s it.
17+
/// </summary>
18+
/// <param name="buffer">The source of the string.</param>
919
private protected StringToken(StringBuilder buffer)
20+
: this(buffer.ToString())
1021
{
11-
this.Value = buffer.ToString();
1222
_ = buffer.Clear();
1323
}
1424
}

SqlServerSimulator/Parser/Tokens/UnquotedString.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,26 @@
22

33
namespace SqlServerSimulator.Parser.Tokens;
44

5-
sealed class UnquotedString(StringBuilder buffer) : Name(buffer)
5+
sealed class UnquotedString : Name
66
{
7+
private UnquotedString(string value) : base(value)
8+
{
9+
}
10+
11+
/// <summary>
12+
/// Returns either an <see cref="UnquotedString"/> or <see cref="ReservedKeyword"/> depending on input.
13+
/// </summary>
14+
/// <param name="buffer">The string to consider. <see cref="StringBuilder.Clear"/> is called.</param>
15+
/// <returns></returns>
16+
public static Token CheckReserved(StringBuilder buffer)
17+
{
18+
var value = buffer.ToString();
19+
_ = buffer.Clear();
20+
21+
return Enum.TryParse<Keyword>(value, true, out var keyword) ?
22+
new ReservedKeyword(keyword, value) :
23+
new UnquotedString(value);
24+
}
25+
726
public override string ToString() => Value;
827
}

SqlServerSimulator/SimulatedSqlException.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ private SimulatedSqlException(string? message, params ReadOnlySpan<SimulatedSqlE
9898

9999
internal static SimulatedSqlException InvalidObjectName(StringToken name) => new($"Invalid object name {name}.", 208, 16, 1);
100100

101+
internal static SimulatedSqlException SyntaxErrorNearKeyword(ReservedKeyword token) => new($"Incorrect syntax near the keyword '{token}'.", 156, 15, 1);
102+
101103
internal static SimulatedSqlException SyntaxErrorNear(Token token) => new($"Incorrect syntax near '{token}'.", 102, 15, 1);
102104

103105
internal static SimulatedSqlException ThereIsAlreadyAnObject(string name) => new($"There is already an object named '{name}' in the database.", 2714, 16, 6);

SqlServerSimulator/Simulation.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
137137
case StatementTerminator:
138138
continue;
139139

140-
case UnquotedString unquotedString:
141-
switch (unquotedString.Parse())
140+
case ReservedKeyword reserved:
141+
switch (reserved.Keyword)
142142
{
143143
case Keyword.Set:
144144
switch (token = tokens.RequireNext())
@@ -150,8 +150,8 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
150150
case "NOCOUNT":
151151
switch (token = tokens.RequireNext())
152152
{
153-
case UnquotedString onOff:
154-
switch (onOff.Parse())
153+
case ReservedKeyword onOff:
154+
switch (onOff.Keyword)
155155
{
156156
case Keyword.On:
157157
case Keyword.Off:
@@ -168,8 +168,8 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
168168
case Keyword.Create:
169169
switch (token = tokens.RequireNext())
170170
{
171-
case UnquotedString whatToCreate:
172-
switch (whatToCreate.Parse())
171+
case ReservedKeyword whatToCreate:
172+
switch (whatToCreate.Keyword)
173173
{
174174
case Keyword.Table:
175175
if (tokens.RequireNext() is not Name tableName)
@@ -194,12 +194,12 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
194194
var nullable = true;
195195

196196
token = tokens.RequireNext();
197-
if (token is UnquotedString next)
197+
if (token is ReservedKeyword next)
198198
{
199-
switch (next.Parse())
199+
switch (next.Keyword)
200200
{
201201
case Keyword.Not:
202-
if ((token = tokens.RequireNext()) is not UnquotedString mustBeNull || mustBeNull.Parse() != Keyword.Null)
202+
if ((token = tokens.RequireNext()) is not ReservedKeyword { Keyword: Keyword.Null })
203203
throw new NotSupportedException($"Simulated command processor doesn't know how to handle column definition token {token}.");
204204

205205
nullable = false;
@@ -237,7 +237,7 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
237237
break;
238238

239239
case Keyword.Insert:
240-
if ((token = tokens.RequireNext()) is UnquotedString maybeInto && maybeInto.TryParse(out var keyword) && keyword == Keyword.Into)
240+
if ((token = tokens.RequireNext()) is ReservedKeyword { Keyword: Keyword.Into })
241241
token = tokens.RequireNext();
242242

243243
if (token is not StringToken destinationTableToken)
@@ -270,7 +270,7 @@ internal IEnumerable<SimulatedStatementOutcome> CreateResultSetsForCommand(Simul
270270
destinationColumns = [.. destinationTable.Columns];
271271
}
272272

273-
if (token is not UnquotedString expectValues || expectValues.Parse() != Keyword.Values)
273+
if (token is not ReservedKeyword { Keyword: Keyword.Values })
274274
break;
275275

276276
if ((token = tokens.RequireNext()) is not OpenParentheses)

0 commit comments

Comments
 (0)