Skip to content

Commit cee4327

Browse files
committed
CREATE SCHEMA + schema-qualified resolution: Database gains a Schemas dict (pre-seeded with dbo) routed via BatchContext.ParseObjectName / TryResolveTable / TryResolveSchema; every table-lookup site (Selection FROM, Insert/Update/Delete/Merge, Drop, Truncate, Create, SelectInto, SET IDENTITY_INSERT, IDENT_CURRENT) consumes the unified pair. Msg 2714 on duplicate, Msg 2760 on reserved-name (dbo/sys/INFORMATION_SCHEMA) and missing-schema CREATE TABLE; Msg 208 / 3701 carry the qualified name with proper single-quoting (also fixed missing quotes in pre-existing Msg 208 wording); Msg 4701 leaf-only quirk preserved. AUTHORIZATION clause and embedded schema_element form raise NotSupportedException — trailing statement-start tokens parse as separate statements (matches the common idiom).
1 parent 0f5c1be commit cee4327

23 files changed

Lines changed: 709 additions & 159 deletions

CLAUDE.md

Lines changed: 23 additions & 6 deletions
Large diffs are not rendered by default.
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Tests for <c>CREATE SCHEMA</c> + schema-qualified table access. Schemas
8+
/// live in <see cref="Database.Schemas"/>; unqualified references resolve
9+
/// through the default <c>dbo</c> entry; two-part <c>schema.table</c> and
10+
/// three-part <c>db.schema.table</c> route through the named schema. Probed
11+
/// against SQL Server 2025 (2026-05-11).
12+
/// </summary>
13+
[TestClass]
14+
public sealed class CreateSchemaTests
15+
{
16+
[TestMethod]
17+
public void CreateSchema_BareForm_Succeeds()
18+
=> AreEqual(1, new Simulation().ExecuteScalar("""
19+
create schema audit;
20+
create table audit.t (id int);
21+
insert audit.t values (1);
22+
select count(*) from audit.t
23+
"""));
24+
25+
[TestMethod]
26+
public void CreateSchema_Duplicate_Msg2714()
27+
=> new Simulation().AssertSqlError(
28+
"create schema audit; create schema audit",
29+
2714,
30+
"There is already an object named 'audit' in the database.");
31+
32+
[TestMethod]
33+
public void CreateSchema_DuplicateCaseInsensitive_Msg2714()
34+
=> new Simulation().AssertSqlError(
35+
"create schema audit; create schema AUDIT",
36+
2714);
37+
38+
[TestMethod]
39+
public void CreateSchema_Dbo_Msg2760()
40+
=> new Simulation().AssertSqlError(
41+
"create schema dbo",
42+
2760,
43+
"The specified schema name \"dbo\" either does not exist or you do not have permission to use it.");
44+
45+
[TestMethod]
46+
public void CreateSchema_Sys_Msg2760()
47+
=> new Simulation().AssertSqlError("create schema sys", 2760);
48+
49+
[TestMethod]
50+
public void CreateSchema_InformationSchema_Msg2760()
51+
=> new Simulation().AssertSqlError("create schema INFORMATION_SCHEMA", 2760);
52+
53+
[TestMethod]
54+
public void CreateSchema_Authorization_NotSupported()
55+
=> Throws<NotSupportedException>(() =>
56+
new Simulation().ExecuteNonQuery("create schema audit authorization dbo"));
57+
58+
/// <summary>
59+
/// Real SQL Server treats trailing tokens after <c>CREATE SCHEMA</c> as
60+
/// the <c>&lt;schema_element&gt;</c> list (inline CREATE TABLE / VIEW /
61+
/// GRANT) — the simulator doesn't implement schema_element grammar, but
62+
/// trailing statement-starting keywords parse as the next statement in the
63+
/// batch (CREATE / SELECT / INSERT / etc. are statement boundaries that
64+
/// the dispatch loop picks up cleanly). Net effect is the same as the
65+
/// common idiom; only the AUTHORIZATION clause and unusual non-boundary
66+
/// trailers raise <see cref="NotSupportedException"/>.
67+
/// </summary>
68+
[TestMethod]
69+
public void CreateSchema_FollowedByCreateTable_DispatchesAsTwoStatements()
70+
=> AreEqual(1, new Simulation().ExecuteScalar("""
71+
create schema audit
72+
create table audit.t (id int)
73+
insert audit.t values (1);
74+
select count(*) from audit.t
75+
"""));
76+
77+
[TestMethod]
78+
public void TwoPartName_Select_Works()
79+
=> AreEqual(42, new Simulation().ExecuteScalar("""
80+
create schema audit;
81+
create table audit.t (id int);
82+
insert audit.t values (42);
83+
select id from audit.t
84+
"""));
85+
86+
[TestMethod]
87+
public void TwoPartName_Insert_Works()
88+
=> AreEqual(2, new Simulation().ExecuteScalar("""
89+
create schema audit;
90+
create table audit.t (id int);
91+
insert audit.t values (1);
92+
insert into audit.t values (2);
93+
select max(id) from audit.t
94+
"""));
95+
96+
[TestMethod]
97+
public void TwoPartName_Update_Works()
98+
=> AreEqual(99, new Simulation().ExecuteScalar("""
99+
create schema audit;
100+
create table audit.t (id int);
101+
insert audit.t values (1);
102+
update audit.t set id = 99;
103+
select id from audit.t
104+
"""));
105+
106+
[TestMethod]
107+
public void TwoPartName_Delete_Works()
108+
=> AreEqual(0, new Simulation().ExecuteScalar("""
109+
create schema audit;
110+
create table audit.t (id int);
111+
insert audit.t values (1), (2), (3);
112+
delete from audit.t;
113+
select count(*) from audit.t
114+
"""));
115+
116+
[TestMethod]
117+
public void TwoPartName_DropTable_Works()
118+
{
119+
using var conn = new Simulation().CreateDbConnection();
120+
conn.Open();
121+
using var cmd = conn.CreateCommand();
122+
cmd.CommandText = """
123+
create schema audit;
124+
create table audit.t (id int);
125+
drop table audit.t
126+
""";
127+
_ = cmd.ExecuteNonQuery();
128+
cmd.CommandText = "create table audit.t (id int)";
129+
_ = cmd.ExecuteNonQuery();
130+
}
131+
132+
[TestMethod]
133+
public void TwoPartName_Truncate_Works()
134+
=> AreEqual(0, new Simulation().ExecuteScalar("""
135+
create schema audit;
136+
create table audit.t (id int);
137+
insert audit.t values (1), (2);
138+
truncate table audit.t;
139+
select count(*) from audit.t
140+
"""));
141+
142+
[TestMethod]
143+
public void TwoPartName_SelectInto_Works()
144+
=> AreEqual(3, new Simulation().ExecuteScalar("""
145+
create schema staging;
146+
create table dbo.src (id int);
147+
insert dbo.src values (1), (2), (3);
148+
select * into staging.dest from dbo.src;
149+
select count(*) from staging.dest
150+
"""));
151+
152+
[TestMethod]
153+
public void ThreePartName_CorrectDatabase_Works()
154+
=> AreEqual(7, new Simulation().ExecuteScalar("""
155+
create schema audit;
156+
create table audit.t (id int);
157+
insert audit.t values (7);
158+
select id from simulated.audit.t
159+
"""));
160+
161+
[TestMethod]
162+
public void ThreePartName_WrongDatabase_Msg208()
163+
=> new Simulation().AssertSqlError("""
164+
create schema audit;
165+
create table audit.t (id int);
166+
select * from baddb.audit.t
167+
""", 208);
168+
169+
[TestMethod]
170+
public void SchemaDoesNotExist_Select_Msg208()
171+
=> new Simulation().AssertSqlError(
172+
"select * from badschema.t",
173+
208,
174+
"Invalid object name 'badschema.t'.");
175+
176+
[TestMethod]
177+
public void SchemaDoesNotExist_CreateTable_Msg2760()
178+
=> new Simulation().AssertSqlError(
179+
"create table badschema.t (id int)",
180+
2760,
181+
"The specified schema name \"badschema\" either does not exist or you do not have permission to use it.");
182+
183+
[TestMethod]
184+
public void SchemaDoesNotExist_Insert_Msg208()
185+
=> new Simulation().AssertSqlError(
186+
"insert badschema.t values (1)",
187+
208);
188+
189+
[TestMethod]
190+
public void SchemaDoesNotExist_DropTable_Msg3701()
191+
=> new Simulation().AssertSqlError(
192+
"drop table badschema.t",
193+
3701,
194+
"Cannot drop the table 'badschema.t', because it does not exist or you do not have permission.");
195+
196+
[TestMethod]
197+
public void SchemaDoesNotExist_DropTableIfExists_NoError()
198+
=> _ = new Simulation().ExecuteNonQuery("drop table if exists badschema.t");
199+
200+
/// <summary>
201+
/// Probe-confirmed: Msg 4701 carries only the leaf in the message,
202+
/// unlike Msg 208 / 3701 which embed the full qualified name.
203+
/// </summary>
204+
[TestMethod]
205+
public void SchemaDoesNotExist_Truncate_Msg4701_LeafNameOnly()
206+
=> new Simulation().AssertSqlError(
207+
"truncate table badschema.t",
208+
4701,
209+
"Cannot find the object \"t\" because it does not exist or you do not have permissions.");
210+
211+
[TestMethod]
212+
public void SchemaExistsTableDoesNot_Select_Msg208()
213+
=> new Simulation().AssertSqlError("""
214+
create schema audit;
215+
select * from audit.nope
216+
""", 208, "Invalid object name 'audit.nope'.");
217+
218+
[TestMethod]
219+
public void IsolatedSchemas_SameTableName()
220+
{
221+
using var reader = new Simulation().ExecuteReader("""
222+
create schema audit;
223+
create schema staging;
224+
create table audit.t (id int);
225+
create table staging.t (id int);
226+
insert audit.t values (1), (2);
227+
insert staging.t values (100);
228+
select (select count(*) from audit.t) as a, (select count(*) from staging.t) as s
229+
""");
230+
IsTrue(reader.Read());
231+
AreEqual(2, reader.GetInt32(0));
232+
AreEqual(1, reader.GetInt32(1));
233+
}
234+
235+
[TestMethod]
236+
public void UnqualifiedReference_ResolvesToDbo()
237+
=> AreEqual(5, new Simulation().ExecuteScalar("""
238+
create schema audit;
239+
create table dbo.t (id int);
240+
create table audit.t (id int);
241+
insert dbo.t values (5);
242+
insert audit.t values (999);
243+
select id from t
244+
"""));
245+
246+
[TestMethod]
247+
public void ExplicitDbo_Equivalent_ToBare()
248+
=> AreEqual(5, new Simulation().ExecuteScalar("""
249+
create table dbo.t (id int);
250+
insert dbo.t values (5);
251+
select id from t
252+
"""));
253+
254+
[TestMethod]
255+
public void CrossSchemaJoin_Works()
256+
=> AreEqual(2, new Simulation().ExecuteScalar("""
257+
create schema audit;
258+
create table dbo.users (id int, name nvarchar(50));
259+
create table audit.entries (user_id int, action nvarchar(50));
260+
insert dbo.users values (1, 'alice'), (2, 'bob');
261+
insert audit.entries values (1, 'login'), (1, 'logout'), (2, 'login');
262+
select count(*) from dbo.users u inner join audit.entries e on u.id = e.user_id where u.id = 1
263+
"""));
264+
265+
[TestMethod]
266+
public void DropSchema_NotSupported()
267+
=> Throws<DbException>(() =>
268+
new Simulation().ExecuteNonQuery("create schema audit; drop schema audit"));
269+
270+
[TestMethod]
271+
public void CreateSchema_PersistsAcrossBatches()
272+
{
273+
using var conn = new Simulation().CreateDbConnection();
274+
conn.Open();
275+
using var cmd = conn.CreateCommand();
276+
cmd.CommandText = "create schema audit";
277+
_ = cmd.ExecuteNonQuery();
278+
cmd.CommandText = "create table audit.t (id int); insert audit.t values (1); select count(*) from audit.t";
279+
AreEqual(1, cmd.ExecuteScalar());
280+
}
281+
}

SqlServerSimulator/Database.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,35 @@ namespace SqlServerSimulator;
1212
/// <see cref="Simulation"/> ships with exactly one entry named
1313
/// <see cref="Simulation.DefaultDatabaseName"/>.
1414
/// </summary>
15-
internal sealed class Database(string name)
15+
internal sealed class Database
1616
{
17+
/// <summary>The schema name an unqualified table reference resolves through.</summary>
18+
public const string DefaultSchemaName = "dbo";
19+
1720
/// <summary>Database name (the key in <see cref="Simulation.Databases"/>).</summary>
18-
public readonly string Name = name;
21+
public readonly string Name;
22+
23+
/// <summary>
24+
/// Namespaces inside this database, keyed by name. Pre-populated with the
25+
/// default <c>dbo</c> schema; <c>CREATE SCHEMA &lt;name&gt;</c> adds more.
26+
/// Schema-qualified table references (<c>SELECT * FROM audit.t</c>) route
27+
/// through here; unqualified references fall back to
28+
/// <see cref="DefaultSchemaName"/>.
29+
/// </summary>
30+
public readonly ConcurrentDictionary<string, Schema> Schemas = new(Collation.Default);
1931

20-
/// <summary>User tables in this database, keyed by name.</summary>
21-
public readonly ConcurrentDictionary<string, HeapTable> HeapTables = new(Collation.Default);
32+
public Database(string name)
33+
{
34+
this.Name = name;
35+
this.Schemas[DefaultSchemaName] = new Schema(DefaultSchemaName);
36+
}
37+
38+
/// <summary>
39+
/// Convenience accessor for the <c>dbo</c> schema's tables — the
40+
/// unqualified-reference fallback path. Equivalent to
41+
/// <c>Schemas[DefaultSchemaName].HeapTables</c>.
42+
/// </summary>
43+
public ConcurrentDictionary<string, HeapTable> DefaultSchemaTables => this.Schemas[DefaultSchemaName].HeapTables;
2244

2345
/// <summary>
2446
/// Database compatibility level. Freshly-constructed databases default

SqlServerSimulator/Errors/SimulatedSqlException.ResolutionErrors.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using SqlServerSimulator.Parser;
2-
using SqlServerSimulator.Parser.Tokens;
32

43
namespace SqlServerSimulator;
54

@@ -21,7 +20,7 @@ internal static SimulatedSqlException IdentifierTooLong(ReadOnlySpan<char> first
2120
internal static SimulatedSqlException AmbiguousColumnName(string name) =>
2221
new($"Ambiguous column name '{name}'.", 209, 16, 1);
2322

24-
internal static SimulatedSqlException InvalidObjectName(StringToken name) => new($"Invalid object name {name}.", 208, 16, 1);
23+
internal static SimulatedSqlException InvalidObjectName(MultiPartName name) => new($"Invalid object name '{name}'.", 208, 16, 1);
2524

2625
internal static SimulatedSqlException MustDeclareScalarVariable(string name) => new($"Must declare the scalar variable \"@{name}\".", 137, 15, 2);
2726

SqlServerSimulator/Errors/SimulatedSqlException.SchemaErrors.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ internal static SimulatedSqlException InvalidCompatibilityLevel() =>
2525

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

28+
/// <summary>
29+
/// Mimics SQL Server error 2760: a statement referenced a schema that
30+
/// doesn't exist (or whose principal the caller can't access). Probe-
31+
/// confirmed wording — real SQL Server uses the same Msg / wording for
32+
/// <c>CREATE TABLE missingschema.t</c>, <c>CREATE SCHEMA dbo</c> (when
33+
/// targeting a built-in / reserved schema), and a few other lookups.
34+
/// </summary>
35+
internal static SimulatedSqlException SpecifiedSchemaNameDoesNotExist(string schemaName) =>
36+
new($"The specified schema name \"{schemaName}\" either does not exist or you do not have permission to use it.", 2760, 16, 1);
37+
2838
/// <summary>
2939
/// Mimics SQL Server error 2705: <c>SELECT … INTO target</c> produced a
3040
/// projection with two columns of the same name. Wording probe-confirmed

0 commit comments

Comments
 (0)