Skip to content

Commit d4106d7

Browse files
committed
Split Simulation.cs into multiple files on specific areas of concern.
1 parent 81d5593 commit d4106d7

9 files changed

Lines changed: 1101 additions & 1034 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using SqlServerSimulator.Parser;
2+
using SqlServerSimulator.Parser.Tokens;
3+
4+
namespace SqlServerSimulator;
5+
6+
partial class Simulation
7+
{
8+
/// <summary>
9+
/// Parses the two ALTER DATABASE forms the simulator currently models:
10+
/// <c>ALTER DATABASE … SET COMPATIBILITY_LEVEL = N</c> (per-database
11+
/// compat) and
12+
/// <c>ALTER DATABASE SCOPED CONFIGURATION SET VERBOSE_TRUNCATION_WARNINGS = ON|OFF</c>.
13+
/// The simulator has a single database, so any database name (including
14+
/// <c>CURRENT</c>) is accepted and ignored.
15+
/// </summary>
16+
private bool TryParseAlter(ParserContext context)
17+
{
18+
if (context.GetNextRequired() is not ReservedKeyword { Keyword: Keyword.Database })
19+
return false;
20+
21+
var afterDatabase = context.GetNextRequired();
22+
if (context.MatchContextual(ContextualKeyword.Scoped))
23+
return TryParseAlterDatabaseScopedConfiguration(context);
24+
25+
// Otherwise a database name (or CURRENT). The simulator has one
26+
// database; accept anything that looks like an identifier.
27+
return afterDatabase is Name or ReservedKeyword { Keyword: Keyword.Current }
28+
&& TryParseAlterDatabaseSet(context);
29+
}
30+
31+
private bool TryParseAlterDatabaseSet(ParserContext context)
32+
{
33+
if (context.GetNextRequired() is not ReservedKeyword { Keyword: Keyword.Set })
34+
return false;
35+
36+
context.MoveNextRequired();
37+
if (!context.MatchContextual(ContextualKeyword.Compatibility_Level))
38+
return false;
39+
40+
if (context.GetNextRequired() is not Operator { Character: '=' })
41+
return false;
42+
43+
if (context.GetNextRequired() is not Numeric { Value: { IsNull: false } numericValue })
44+
return false;
45+
46+
var requested = numericValue.AsInt32;
47+
if (!Enum.IsDefined((CompatibilityLevel)requested))
48+
throw SimulatedSqlException.InvalidCompatibilityLevel();
49+
50+
this.CompatibilityLevel = (CompatibilityLevel)requested;
51+
return true;
52+
}
53+
54+
private bool TryParseAlterDatabaseScopedConfiguration(ParserContext context)
55+
{
56+
context.MoveNextRequired();
57+
if (!context.MatchContextual(ContextualKeyword.Configuration))
58+
return false;
59+
60+
if (context.GetNextRequired() is not ReservedKeyword { Keyword: Keyword.Set })
61+
return false;
62+
63+
context.MoveNextRequired();
64+
if (!context.MatchContextual(ContextualKeyword.Verbose_Truncation_Warnings))
65+
return false;
66+
67+
if (context.GetNextRequired() is not Operator { Character: '=' })
68+
return false;
69+
70+
if (context.GetNextRequired() is not ReservedKeyword { Keyword: var on } || on is not (Keyword.On or Keyword.Off))
71+
return false;
72+
73+
this.VerboseTruncationWarnings = on == Keyword.On;
74+
return true;
75+
}
76+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using SqlServerSimulator.Storage;
2+
3+
namespace SqlServerSimulator;
4+
5+
partial class Simulation
6+
{
7+
/// <summary>
8+
/// Coerces an auto-generated identity <see cref="long"/> to the column's
9+
/// declared integer type, raising the IDENTITY-specific Msg 8115 if the
10+
/// next value won't fit.
11+
/// </summary>
12+
private static SqlValue CoerceForIdentity(long value, HeapColumn identityColumn)
13+
{
14+
try
15+
{
16+
return SqlValue.FromInt64(value).CoerceTo(identityColumn.Type);
17+
}
18+
catch (OverflowException)
19+
{
20+
throw SimulatedSqlException.IdentityOverflow(identityColumn.Type.ToString()!);
21+
}
22+
}
23+
24+
/// <summary>
25+
/// Raises a truncation error when the SOURCE value's natural length would
26+
/// exceed <paramref name="column"/>'s declared maximum. The check fires
27+
/// pre-coerce so that <c>char(N)</c> / <c>nchar(N)</c> / <c>binary(N)</c>
28+
/// columns — whose CoerceTo silently truncates to match SQL Server's CAST
29+
/// semantics — still raise the bind-time truncation error. NULL values
30+
/// and columns without a declared max are no-ops. Selects between the
31+
/// verbose Msg 2628 (with table/column/value) and the legacy Msg 8152 via
32+
/// <see cref="IsVerboseTruncationActive"/>.
33+
/// </summary>
34+
/// <remarks>
35+
/// Length unit follows the column's storage encoding: CP1252 byte count
36+
/// for <c>varchar</c> / <c>char(N)</c>, raw byte count for <c>varbinary</c>
37+
/// / <c>binary(N)</c>, UCS-2 code units (<see cref="string.Length"/>) for
38+
/// <c>nvarchar</c> / <c>nchar(N)</c> / <c>sysname</c>. Non-string sources
39+
/// fall through (e.g. <c>INSERT INTO varchar(5) VALUES (12345)</c>): the
40+
/// integer-to-string format path inside <c>CoerceTo</c> produces a value
41+
/// the column can hold for the common cases, and any genuine overflow
42+
/// surfaces as a coercion error instead.
43+
/// </remarks>
44+
private static void EnforceMaxLength(SqlValue source, HeapColumn column, string tableName, Simulation simulation)
45+
{
46+
if (source.IsNull || column.MaxLength is not int max)
47+
return;
48+
49+
int actual;
50+
if (column.Type == SqlType.Varbinary || column.Type is BinarySqlType)
51+
{
52+
if (source.Type is not (VarbinarySqlType or BinarySqlType))
53+
return;
54+
actual = source.AsBytes.Length;
55+
}
56+
else if (column.Type == SqlType.Varchar || column.Type is CharSqlType)
57+
{
58+
if (source.Type.Category != SqlTypeCategory.String)
59+
return;
60+
actual = SqlType.Varchar.GetVariableByteCount(SqlValue.FromVarchar(source.AsString));
61+
}
62+
else
63+
{
64+
if (source.Type.Category != SqlTypeCategory.String)
65+
return;
66+
actual = source.AsString.Length;
67+
}
68+
69+
if (actual <= max)
70+
return;
71+
72+
if (!simulation.IsVerboseTruncationActive())
73+
throw SimulatedSqlException.StringOrBinaryWouldBeTruncatedLegacy();
74+
75+
throw column.Type == SqlType.Varbinary || column.Type is BinarySqlType
76+
? SimulatedSqlException.StringOrBinaryWouldBeTruncated(tableName, column.Name, source.AsBytes, max)
77+
: SimulatedSqlException.StringOrBinaryWouldBeTruncated(tableName, column.Name, source.AsString, max);
78+
}
79+
80+
/// <summary>
81+
/// Coerces an INSERT source value to the destination column's type,
82+
/// converting any overflow into the SQL Server-shaped Msg 8115. Truncation
83+
/// of strings/bytes is handled separately by <see cref="EnforceMaxLength"/>
84+
/// before this method runs.
85+
/// </summary>
86+
private static SqlValue CoerceForInsert(SqlValue source, SqlType targetType)
87+
{
88+
try
89+
{
90+
return source.CoerceTo(targetType);
91+
}
92+
catch (OverflowException)
93+
{
94+
throw SimulatedSqlException.ArithmeticOverflow(targetType.ToString()!);
95+
}
96+
}
97+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using SqlServerSimulator.Parser;
2+
using SqlServerSimulator.Parser.Tokens;
3+
using SqlServerSimulator.Storage;
4+
5+
namespace SqlServerSimulator;
6+
7+
partial class Simulation
8+
{
9+
/// <summary>
10+
/// Parses <c>CREATE TABLE</c>. Returns false if the leading <c>CREATE</c>
11+
/// isn't followed by <c>TABLE</c> (so the caller can route to the syntax
12+
/// error). Other malformed forms throw <see cref="SimulatedSqlException"/>
13+
/// directly with the matching SQL Server error.
14+
/// </summary>
15+
private bool TryParseCreate(ParserContext context)
16+
{
17+
if (context.GetNextRequired() is not ReservedKeyword { Keyword: Keyword.Table })
18+
return false;
19+
20+
if (context.GetNextRequired() is not Name tableName)
21+
return false;
22+
23+
if (context.GetNextRequired() is not Operator { Character: '(' })
24+
return false;
25+
26+
var rawColumns = new List<(Name Name, Name TypeName, int? DeclaredMaxLength, int? DeclaredScale, bool Nullable, IdentityState? Identity)>();
27+
bool suppressAdvanceToken;
28+
do
29+
{
30+
suppressAdvanceToken = false;
31+
var columnName = context.GetNextRequired<Name>();
32+
var type = context.GetNextRequired<Name>();
33+
34+
int? declaredMaxLength = null;
35+
int? declaredScale = null;
36+
context.MoveNextRequired();
37+
if (context.Token is Operator { Character: '(' })
38+
{
39+
var lengthToken = context.GetNextRequired();
40+
if (lengthToken is Numeric { Value: { IsNull: false } numericValue })
41+
{
42+
declaredMaxLength = numericValue.AsInt32;
43+
}
44+
else if (context.MatchContextual(ContextualKeyword.Max))
45+
{
46+
throw new NotSupportedException($"{type}(MAX) and other LOB types aren't modeled yet.");
47+
}
48+
else
49+
{
50+
throw SimulatedSqlException.SyntaxErrorNear(context);
51+
}
52+
53+
switch (context.GetNextRequired())
54+
{
55+
case Operator { Character: ',' }:
56+
if (context.GetNextRequired() is not Numeric { Value: { IsNull: false } scaleValue })
57+
throw SimulatedSqlException.SyntaxErrorNear(context);
58+
declaredScale = scaleValue.AsInt32;
59+
if (context.GetNextRequired() is not Operator { Character: ')' })
60+
throw SimulatedSqlException.SyntaxErrorNear(context);
61+
break;
62+
case Operator { Character: ')' }:
63+
break;
64+
default:
65+
throw SimulatedSqlException.SyntaxErrorNear(context);
66+
}
67+
68+
context.MoveNextRequired();
69+
}
70+
71+
IdentityState? identity = null;
72+
if (context.Token is ReservedKeyword { Keyword: Keyword.Identity })
73+
{
74+
identity = ParseIdentitySpec(context, columnName.Value);
75+
}
76+
77+
bool nullable;
78+
if (context.Token is ReservedKeyword next)
79+
{
80+
switch (next.Keyword)
81+
{
82+
case Keyword.Not:
83+
if (context.GetNextRequired() is not ReservedKeyword { Keyword: Keyword.Null })
84+
throw SimulatedSqlException.SyntaxErrorNear(context);
85+
86+
nullable = false;
87+
break;
88+
case Keyword.Null:
89+
nullable = true;
90+
break;
91+
default:
92+
throw SimulatedSqlException.SyntaxErrorNear(context);
93+
}
94+
}
95+
else
96+
{
97+
suppressAdvanceToken = true;
98+
nullable = identity is null;
99+
}
100+
101+
rawColumns.Add((columnName, type, declaredMaxLength, declaredScale, nullable, identity));
102+
} while ((suppressAdvanceToken ? context.Token : context.GetNextRequired()) is Operator { Character: ',' });
103+
104+
if (context.Token is not Operator { Character: ')' })
105+
return false;
106+
107+
var heapColumns = new HeapColumn[rawColumns.Count];
108+
var fixedWidthSum = 0;
109+
var identityCount = 0;
110+
for (var i = 0; i < rawColumns.Count; i++)
111+
{
112+
var raw = rawColumns[i];
113+
var (resolvedType, maxLength) = SqlType.GetByName(raw.TypeName, raw.DeclaredMaxLength, raw.DeclaredScale, i + 1, raw.Name.Value);
114+
if (raw.Identity is not null)
115+
{
116+
if (++identityCount > 1)
117+
throw SimulatedSqlException.MultipleIdentityColumns(tableName.Value);
118+
if (raw.Nullable)
119+
throw SimulatedSqlException.IdentityOnNullableColumn(raw.Name.Value, tableName.Value);
120+
if (resolvedType != SqlType.Int32 && resolvedType != SqlType.BigInt && resolvedType != SqlType.SmallInt && resolvedType != SqlType.TinyInt)
121+
throw SimulatedSqlException.IdentityInvalidType(raw.Name.Value);
122+
}
123+
heapColumns[i] = new(raw.Name.Value, resolvedType, maxLength, raw.Nullable, raw.Identity);
124+
if (resolvedType.IsFixedLength)
125+
fixedWidthSum += resolvedType.FixedLength;
126+
}
127+
128+
// Schemas whose fixed-width columns alone exceed SQL Server's 8060-byte
129+
// in-row limit can never hold a row; reject at CREATE TABLE (Msg 1701).
130+
// The variable-width-aware warning path is deferred until warning
131+
// infrastructure exists.
132+
if (fixedWidthSum > Heap.MaxRowSize)
133+
throw SimulatedSqlException.RowSizeExceedsMaximum(tableName.Value, fixedWidthSum, Heap.MaxRowSize);
134+
135+
var heapTable = new HeapTable(tableName.Value, heapColumns);
136+
return this.HeapTables.TryAdd(heapTable.Name, heapTable)
137+
? true
138+
: throw SimulatedSqlException.ThereIsAlreadyAnObject(heapTable.Name);
139+
}
140+
141+
/// <summary>
142+
/// Parses the <c>IDENTITY [(seed, increment)]</c> property after a column's
143+
/// data type. Enters with <see cref="ParserContext.Token"/> on the
144+
/// <c>IDENTITY</c> keyword and leaves it on the next non-identity token
145+
/// (a nullability keyword, comma, or the column-list's closing paren).
146+
/// Bare <c>IDENTITY</c> is shorthand for <c>IDENTITY(1, 1)</c>.
147+
/// </summary>
148+
private static IdentityState ParseIdentitySpec(ParserContext context, string columnName)
149+
{
150+
long seed = 1;
151+
long increment = 1;
152+
var afterIdentity = context.GetNextRequired();
153+
if (afterIdentity is Operator { Character: '(' })
154+
{
155+
context.MoveNextRequired();
156+
seed = EvaluateLiteralBigInt(Expression.Parse(context));
157+
if (context.Token is not Operator { Character: ',' })
158+
throw SimulatedSqlException.SyntaxErrorNear(context);
159+
context.MoveNextRequired();
160+
increment = EvaluateLiteralBigInt(Expression.Parse(context));
161+
if (context.Token is not Operator { Character: ')' })
162+
throw SimulatedSqlException.SyntaxErrorNear(context);
163+
context.MoveNextRequired();
164+
}
165+
return increment == 0
166+
? throw SimulatedSqlException.IdentityInvalidIncrement(columnName)
167+
: new IdentityState(seed, increment);
168+
}
169+
170+
private static long EvaluateLiteralBigInt(Expression expression) =>
171+
expression.Run(name => throw SimulatedSqlException.InvalidColumnName(name)).CoerceTo(SqlType.BigInt).AsInt64;
172+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using SqlServerSimulator.Parser;
2+
using SqlServerSimulator.Parser.Tokens;
3+
4+
namespace SqlServerSimulator;
5+
6+
partial class Simulation
7+
{
8+
/// <summary>
9+
/// Parses <c>DBCC TRACEON(N)</c> / <c>DBCC TRACEOFF(N)</c>. The optional
10+
/// <c>, -1</c> suffix that promotes the flag to global scope isn't modeled
11+
/// — the simulator has a single connection so session vs global doesn't
12+
/// matter today.
13+
/// </summary>
14+
private bool TryParseDbcc(ParserContext context)
15+
{
16+
context.MoveNextRequired();
17+
bool turningOn;
18+
switch (context.AsContextual())
19+
{
20+
case ContextualKeyword.TraceOn: turningOn = true; break;
21+
case ContextualKeyword.TraceOff: turningOn = false; break;
22+
default: return false;
23+
}
24+
25+
if (context.GetNextRequired() is not Operator { Character: '(' })
26+
return false;
27+
28+
if (context.GetNextRequired() is not Numeric { Value: { IsNull: false } numericValue })
29+
return false;
30+
31+
if (context.GetNextRequired() is not Operator { Character: ')' })
32+
return false;
33+
34+
var flag = numericValue.AsInt32;
35+
_ = turningOn ? this.TraceFlags.Add(flag) : this.TraceFlags.Remove(flag);
36+
return true;
37+
}
38+
}

0 commit comments

Comments
 (0)