Skip to content

Commit 14e362f

Browse files
committed
ALTER TABLE ADD [COLUMN] col TYPE [, …] / DROP COLUMN [IF EXISTS] col [, …] — closes the largest remaining EF Migrations DDL gap. ParseColumnList refactored to extract ParseOneColumnIntoLists (the per-column parsing body — name + type + inline constraint loop), reusable from both CREATE TABLE and the new ALTER-side parser; CREATE TABLE behavior unchanged. New Simulation.AlterTableColumn.cs partial drives both shapes from the existing TryParseAlterTableAddConstraint / TryParseAlterTableDropConstraint dispatchers, routing on Keyword.Column (or any non-constraint leading identifier for the bare ADD col … form). HeapTable.Columns / StoredColumns / StorageOrdinals / Schema / Heap dropped readonly (they were already populated post-construction by the constructor's projection loop; now also mutated by ALTER TABLE column ops); new RecomputeStorageProjections() helper encapsulates the projection invariant. ADD COLUMN supports multi-column comma list, the optional COLUMN keyword, IDENTITY (seed/increment), inline DEFAULT (named or auto), inline CHECK / UNIQUE / PRIMARY KEY / REFERENCES, and computed columns (col AS expr [PERSISTED [NOT NULL]]); existing-table identity count seeded from HeapTable.IdentityOrdinal so a second IDENTITY raises Msg 2744. Backfill semantic matches probe-confirmed SQL Server quirks: nullable ADD always backfills NULL (DEFAULT applies only to future inserts), NOT NULL with DEFAULT evaluates once and snapshots the same value to every existing row (GETUTCDATE produces one timestamp not per-row), IDENTITY backfills seed / seed+inc / … in heap-scan order, ROWVERSION pulls per-row from the database-scoped counter, and NOT NULL without DEFAULT/IDENTITY/ROWVERSION on a non-empty table raises Msg 4901 (full canonical paragraph wording). Msg 2705 fires on column-name collision with an existing column OR a duplicate within the same multi-column ADD. New PK / UQ / CHECK / FK declarations route through the existing ResolveKeyConstraints / ResolveCheckConstraints / ResolveForeignKeys pipelines after shifting their FullOrdinals by existing column count so the resolvers see the post-add combined column view; any resolver failure (Msg 1505 unique-on-duplicates, etc.) unwinds the schema mutation via a try/catch that restores table.Columns + RecomputeStorageProjections + truncates the appended constraints. Three MoveNextRequired → MoveNextOptional changes in the shared column parser (after type-length close paren, after NOT NULL, after bare NULL, after IDENTITY's close paren, after inline CHECK's close paren) let the column parser terminate cleanly at end-of-batch — CREATE TABLE still hits the close ) of the column list, ALTER ADD COLUMN can run out of tokens. DROP COLUMN is two-pass-atomic: pass 1 resolves every name (Msg 4924 on miss unless IF EXISTS) and walks each column's dependencies via CollectColumnDependencies (PK/UQ via StorageOrdinals, outgoing FK via ChildColumnOrdinals, incoming FK via ReferencedColumnOrdinals on this table, CHECK via InlineColumn + structural predicate walk for table-level CHECKs, DEFAULT via HeapColumn.DefaultConstraint, index via Index.KeyColumns + IncludedColumns); pass 2 applies the mutation only when every column cleared. Msg 5074 lists every blocker on its own line with the probe-confirmed prefix split (The object 'X' for constraints, The index 'X' for indexes) — DropColumnHasDependenciesMixed accepts a per-blocker (name, isIndex) tuple. Surviving constraint / index / FK ordinals remap in place via oldStorageToNew[] / oldFullToNew[] arrays — KeyConstraint.StorageOrdinals elements, Index.KeyColumns slot replacement, Index.IncludedColumns elements, ForeignKey.ChildColumnOrdinals (outgoing) and ReferencedColumnOrdinals (incoming) all assigned. Heap re-encoded by decoding each row under the pre-drop StoredColumns layout, projecting through surviving ordinals, and encoding against the new StoredColumns; Heap field replaced wholesale. Five new error factories: AddColumnRequiresDefaultOrNullable (4901), DropColumnDoesNotExist (4924), DropColumnHasDependenciesMixed (5074 with multi-blocker enumeration), ColumnNamesMustBeUnique (2705); existing factories CannotFindObjectForAlterTable (4902), MultipleIdentityColumns (2744), PrimaryKeyOnNullableColumn (8111), DuplicateKeyOnCreate (1505) all reused. 33 new AlterTableColumnTests cover ADD (nullable / NOT NULL with and without DEFAULT / GETUTCDATE constant-snapshot / 4901 path / IDENTITY backfill / 2744 second-identity / optional COLUMN keyword / multi-column comma list / 2705 duplicate / inline CHECK / inline FK / computed column / sys.columns visibility) and DROP (basic remove / row preservation / 4924 missing / IF EXISTS / 5074 across DEFAULT/CHECK/PK/outgoing-FK/incoming-FK/index-key/index-include / multi-column atomicity / IDENTITY drop / round-trip ADD+DROP / middle-column drop preserving remaining-column read + PK enforcement + UNIQUE index enforcement). Two pre-existing tests asserting NotSupportedException on ALTER TABLE ADD COLUMN updated (one removed entirely; one re-targeted to ALTER COLUMN which is still deferred). CLAUDE.md "Not modeled" entry rewritten — ADD / DROP COLUMN listed alongside the already-shipped ADD / DROP CONSTRAINT shapes; ALTER COLUMN remains the only column-mutation shape still raising NotSupportedException. docs/claude/alter-table.md gains a "Column ops" section with the grammar, backfill semantic table, error-path matrix, dependency-rejection enumeration, storage rewrite mechanics, and four documented fidelity gaps (eager rewrite vs metadata-only optimization, non-transactional column DDL, table-variable column ops, single-primary-error vs trailing-informational pair).
1 parent c7f5ec3 commit 14e362f

10 files changed

Lines changed: 1467 additions & 229 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ Per-feature deep-dives live under `docs/claude/`. Each entry below is a trigger:
178178
- T-SQL `GOTO` / labels — `IF` / `BEGIN…END` / `WHILE` / `BREAK` / `CONTINUE` / `RETURN` (bare + UDF-body and stored-procedure-body value form) ship; unconditional jumps don't.
179179
- CLR functions, INSTEAD OF UPDATE / DELETE on *non-updatable* views (INSTEAD OF INSERT on any view ships; INSTEAD OF UPDATE / DELETE on updatable single-base views ships). DDL / logon triggers also unmodeled. Scalar UDFs, inline TVFs, multi-statement TVFs (`RETURNS @r TABLE (cols) AS BEGIN ... END` — EF Core `HasDbFunction` end-to-end), views, DML-through-views (single-source updatable shape), stored procedures (including CREATE / ALTER / DROP, EXEC with input/output/default params, `@rc = EXEC` return-code capture, `CommandType.StoredProcedure`, `EXEC (@sql)` and `sp_executesql` dynamic SQL), user-defined table types + table-valued parameters (CREATE TYPE … AS TABLE / DROP TYPE / DECLARE @t MyType / READONLY proc params / EXEC with TVP arg / ADO.NET Structured parameter via the `DbParameter.TypeName` C# 14 extension property + DataTable/IDataReader sources), sequence objects (CREATE / ALTER / DROP SEQUENCE / NEXT VALUE FOR / sys.sequences — supports EF Core HiLo identity strategy end-to-end), and DML triggers (CREATE / ALTER / CREATE OR ALTER / DROP TRIGGER, FOR-as-AFTER synonym, AFTER on heap tables + INSTEAD OF on heap tables / views, multi-action INSERT,UPDATE,DELETE, INSERTED/DELETED pseudo-tables, multiple AFTER triggers per parent / one INSTEAD OF per action per target (Msg 2111), DISABLE/ENABLE TRIGGER, TRIGGER_NESTLEVEL(), direct-recursion suppression matching RECURSIVE_TRIGGERS OFF, sys.triggers + sys.objects integration, trigger-error rolls back firing DML, MERGE per-action routing through INSTEAD OF, DROP TABLE / DROP VIEW cascade-drops attached triggers) ship (see `docs/claude/programmable.md`, `docs/claude/table-valued-parameters.md`, `docs/claude/sequences.md`, and `docs/claude/triggers.md`). JOIN-view single-base-table UPDATE/DELETE, OUTPUT through views, and multi-source alias-form UPDATE/DELETE through views are deferred (Msg 4405 or `NotSupportedException` at the DML site). `BEGIN ATOMIC` / `BEGIN DISTRIBUTED TRANSACTION` raise `NotSupportedException` at dispatch. Value-form `RETURN N` is legal inside a scalar-UDF body and a stored-procedure body; bare batch / dynamic-SQL scope raises Msg 178. TRY/CATCH + THROW + RAISERROR (printf-style `%s %d/%i %u %o %x %X %% %ld %I64d` with width / precision / left-align / zero-pad; severity routing 0-10 informational vs 11-18 catchable; `WITH SETERROR`/`NOWAIT` ship, `WITH LOG` uniformly raises Msg 2778; numeric `msg_id` raises Msg 2732 for 50000 / `< 13000` and Msg 18054 otherwise — `sys.messages` registry / `sp_addmessage` not modeled) + live `@@ERROR` + `ERROR_*()` functions ship (see `docs/claude/control-flow.md`).
180180
- **`PRINT` message capture** — the statement parses + evaluates the operand (so operand-side errors like Msg 245 surface), but the message is discarded. `DbConnection` has no `InfoMessage` event (that's a `SqlConnection` extension), so adding a public observability surface would mean a new event on `SimulatedDbConnection`. Defer until an application needs it.
181-
- **`ALTER TABLE` grammar beyond SET / ADD / DROP / CHECK / NOCHECK CONSTRAINT**`SET (SYSTEM_VERSIONING = OFF)` (see [`docs/claude/temporal-tables.md`](docs/claude/temporal-tables.md)), `[WITH CHECK | WITH NOCHECK] ADD [CONSTRAINT name] (PRIMARY KEY | UNIQUE | FOREIGN KEY | CHECK | DEFAULT) …`, `DROP CONSTRAINT [IF EXISTS] name [, …]`, and `[WITH CHECK | WITH NOCHECK] (CHECK | NOCHECK) CONSTRAINT (ALL | name [, …])` (trust toggling for bulk imports — sets / clears `is_disabled` + `is_not_trusted`; see [`docs/claude/alter-table.md`](docs/claude/alter-table.md)) all ship. ADD / DROP COLUMN, ALTER COLUMN, DROP PERIOD FOR SYSTEM_TIME, REBUILD, SWITCH PARTITION, the SET-versioning-on direction, and every other shape raise `NotSupportedException`. Multi-constraint ADD (`ADD CONSTRAINT pk1 PK (a), CONSTRAINT fk1 FK …`) also raises.
181+
- **`ALTER TABLE` grammar beyond SET / ADD / DROP / CHECK / NOCHECK CONSTRAINT** — `SET (SYSTEM_VERSIONING = OFF)` (see [`docs/claude/temporal-tables.md`](docs/claude/temporal-tables.md)), `[WITH CHECK | WITH NOCHECK] ADD [CONSTRAINT name] (PRIMARY KEY | UNIQUE | FOREIGN KEY | CHECK | DEFAULT) …`, `DROP CONSTRAINT [IF EXISTS] name [, …]`, `[WITH CHECK | WITH NOCHECK] (CHECK | NOCHECK) CONSTRAINT (ALL | name [, …])` (trust toggling for bulk imports — sets / clears `is_disabled` + `is_not_trusted`), `ADD [COLUMN] col TYPE [, …]` (multi-column add — inline DEFAULT / IDENTITY / CHECK / FK / computed all supported; Msg 4901 on NOT NULL without DEFAULT on non-empty table; eager row rewrite with NULL backfill for nullable adds and constant-snapshot DEFAULT for NOT NULL), and `DROP COLUMN [IF EXISTS] col [, …]` (Msg 4924 missing column, Msg 5074 dependency rejection with multi-blocker enumeration across PK/UQ/FK/CHECK/DEFAULT/index, atomic multi-drop) all ship (see [`docs/claude/alter-table.md`](docs/claude/alter-table.md)). ALTER COLUMN, DROP PERIOD FOR SYSTEM_TIME, REBUILD, SWITCH PARTITION, the SET-versioning-on direction, and every other shape raise `NotSupportedException`. Multi-constraint ADD (`ADD CONSTRAINT pk1 PK (a), CONSTRAINT fk1 FK …`) also raises.
182182
- `hierarchyid`, `geography`, `geometry`.
183183

184184
## Quirks (modeled, not byte-identical to SQL Server)
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
using System.Data.Common;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Behavioral tests for <c>ALTER TABLE ADD [COLUMN] col TYPE [...]</c> +
8+
/// <c>ALTER TABLE DROP COLUMN [IF EXISTS] col [, col...]</c>. Probed wording
9+
/// sourced from SQL Server 2025 on 2026-05-14.
10+
/// </summary>
11+
[TestClass]
12+
public sealed class AlterTableColumnTests
13+
{
14+
// --- ADD COLUMN — basic shapes ---
15+
16+
[TestMethod]
17+
public void AddNullableColumn_BackfillsAsNull()
18+
=> AreEqual(2, new Simulation().ExecuteScalar("""
19+
create table t (id int not null primary key, name nvarchar(50));
20+
insert t values (1, 'a'), (2, 'b');
21+
alter table t add note nvarchar(100);
22+
select count(*) from t where note is null
23+
"""));
24+
25+
[TestMethod]
26+
public void AddNullableColumn_WithDefault_DoesNotBackfill()
27+
{
28+
var sim = new Simulation();
29+
// SQL Server quirk: DEFAULT on a nullable ADD only applies to
30+
// future inserts; existing rows stay NULL even though the column
31+
// has a DEFAULT definition.
32+
_ = sim.ExecuteNonQuery("""
33+
create table t (id int not null primary key);
34+
insert t values (1), (2);
35+
alter table t add tag int default 99
36+
""");
37+
AreEqual(2, sim.ExecuteScalar("select count(*) from t where tag is null"));
38+
_ = sim.ExecuteNonQuery("insert t (id) values (3)");
39+
AreEqual(99, sim.ExecuteScalar("select tag from t where id = 3"));
40+
}
41+
42+
[TestMethod]
43+
public void AddNotNullColumn_WithDefault_BackfillsExistingRows()
44+
=> AreEqual(2, new Simulation().ExecuteScalar("""
45+
create table t (id int not null primary key);
46+
insert t values (1), (2);
47+
alter table t add qty int not null default 0;
48+
select count(*) from t where qty = 0
49+
"""));
50+
51+
[TestMethod]
52+
public void AddNotNullColumn_DefaultGetUtcDate_ProducesConstantSnapshot()
53+
=> AreEqual(1, new Simulation().ExecuteScalar("""
54+
create table t (id int not null primary key);
55+
insert t values (1), (2), (3);
56+
alter table t add created datetime not null default getutcdate();
57+
select count(distinct created) from t
58+
"""));
59+
60+
[TestMethod]
61+
public void AddNotNullColumn_WithoutDefault_OnNonEmptyTable_RaisesMsg4901()
62+
=> _ = new Simulation().AssertSqlError("""
63+
create table t (id int not null primary key);
64+
insert t values (1);
65+
alter table t add x int not null
66+
""", 4901);
67+
68+
[TestMethod]
69+
public void AddNotNullColumn_WithoutDefault_OnEmptyTable_Succeeds()
70+
=> AreEqual(0, new Simulation().ExecuteScalar("""
71+
create table t (id int not null primary key);
72+
alter table t add x int not null;
73+
select count(*) from t
74+
"""));
75+
76+
[TestMethod]
77+
public void AddIdentityColumn_BackfillsSequential()
78+
{
79+
var sim = new Simulation();
80+
_ = sim.ExecuteNonQuery("""
81+
create table t (id int not null primary key);
82+
insert t values (10), (20), (30);
83+
alter table t add iden int identity(100, 1)
84+
""");
85+
AreEqual(3, sim.ExecuteScalar("select count(*) from t"));
86+
AreEqual(3, sim.ExecuteScalar("select count(distinct iden) from t"));
87+
AreEqual(100, sim.ExecuteScalar("select min(iden) from t"));
88+
AreEqual(102, sim.ExecuteScalar("select max(iden) from t"));
89+
}
90+
91+
[TestMethod]
92+
public void AddSecondIdentityColumn_RaisesMsg2744()
93+
=> _ = new Simulation().AssertSqlError("""
94+
create table t (id int not null primary key identity);
95+
alter table t add iden2 int identity(1, 1)
96+
""", 2744);
97+
98+
[TestMethod]
99+
public void AddColumn_OptionalColumnKeyword_Accepted()
100+
=> AreEqual(2, new Simulation().ExecuteScalar("""
101+
create table t (id int not null primary key);
102+
insert t values (1), (2);
103+
alter table t add column note nvarchar(50);
104+
select count(*) from t where note is null
105+
"""));
106+
107+
[TestMethod]
108+
public void AddMultipleColumns_CommaList_AllAdded()
109+
{
110+
var sim = new Simulation();
111+
_ = sim.ExecuteNonQuery("""
112+
create table t (id int not null primary key);
113+
insert t values (1);
114+
alter table t add a int, b int default 99
115+
""");
116+
AreEqual(1, sim.ExecuteScalar("select count(*) from t where a is null"));
117+
// Nullable with DEFAULT: existing row stays NULL.
118+
AreEqual(1, sim.ExecuteScalar("select count(*) from t where b is null"));
119+
}
120+
121+
[TestMethod]
122+
public void AddColumn_DuplicateName_RaisesMsg2705()
123+
=> _ = new Simulation().AssertSqlError("""
124+
create table t (id int not null primary key, name nvarchar(50));
125+
alter table t add name nvarchar(100)
126+
""", 2705);
127+
128+
[TestMethod]
129+
public void AddColumn_InlineCheck_AppliedToFutureInserts()
130+
{
131+
var sim = new Simulation();
132+
_ = sim.ExecuteNonQuery("""
133+
create table t (id int not null primary key);
134+
alter table t add qty int constraint ck_qty check (qty is null or qty > 0)
135+
""");
136+
_ = sim.ExecuteNonQuery("insert t values (1, 5)");
137+
var ex = Throws<DbException>(() => sim.ExecuteNonQuery("insert t values (2, -5)"));
138+
AreEqual("547", ex.Data["HelpLink.EvtID"]);
139+
}
140+
141+
[TestMethod]
142+
public void AddColumn_InlineForeignKey_Enforced()
143+
{
144+
var sim = new Simulation();
145+
_ = sim.ExecuteNonQuery("""
146+
create table p (pid int not null primary key);
147+
create table c (id int not null primary key);
148+
insert p values (1);
149+
alter table c add p_ref int constraint fk_pref references p(pid);
150+
insert c values (10, 1)
151+
""");
152+
var ex = Throws<DbException>(() => sim.ExecuteNonQuery("insert c values (20, 999)"));
153+
AreEqual("547", ex.Data["HelpLink.EvtID"]);
154+
}
155+
156+
[TestMethod]
157+
public void AddColumn_ComputedColumn_VisibleFromSelect()
158+
=> AreEqual(20, new Simulation().ExecuteScalar("""
159+
create table t (id int not null primary key);
160+
insert t values (10);
161+
alter table t add doubled as (id * 2);
162+
select doubled from t
163+
"""));
164+
165+
[TestMethod]
166+
public void AddColumn_VisibleInSysColumns()
167+
{
168+
var sim = new Simulation();
169+
_ = sim.ExecuteNonQuery("""
170+
create table t (id int not null primary key);
171+
alter table t add a int, b nvarchar(50)
172+
""");
173+
AreEqual(3, sim.ExecuteScalar("select count(*) from sys.columns where object_id = object_id('t')"));
174+
}
175+
176+
// --- DROP COLUMN — basic shapes ---
177+
178+
[TestMethod]
179+
public void DropColumn_RemovesFromTable()
180+
{
181+
var sim = new Simulation();
182+
_ = sim.ExecuteNonQuery("""
183+
create table t (id int not null primary key, a int, b int);
184+
insert t values (1, 10, 100);
185+
alter table t drop column b
186+
""");
187+
AreEqual(2, sim.ExecuteScalar("select count(*) from sys.columns where object_id = object_id('t')"));
188+
AreEqual(10, sim.ExecuteScalar("select a from t"));
189+
}
190+
191+
[TestMethod]
192+
public void DropColumn_PreservesExistingRows()
193+
=> AreEqual(2, new Simulation().ExecuteScalar("""
194+
create table t (id int not null primary key, name nvarchar(50));
195+
insert t values (1, 'a'), (2, 'b');
196+
alter table t drop column name;
197+
select count(*) from t
198+
"""));
199+
200+
[TestMethod]
201+
public void DropColumn_DoesNotExist_RaisesMsg4924()
202+
=> _ = new Simulation().AssertSqlError("""
203+
create table t (id int not null primary key);
204+
alter table t drop column missing
205+
""", 4924);
206+
207+
[TestMethod]
208+
public void DropColumn_IfExists_Missing_Silent()
209+
=> _ = new Simulation().ExecuteNonQuery("""
210+
create table t (id int not null primary key);
211+
alter table t drop column if exists missing
212+
""");
213+
214+
[TestMethod]
215+
public void DropColumn_WithDefaultConstraint_RaisesMsg5074()
216+
{
217+
var ex = new Simulation().AssertSqlError("""
218+
create table t (id int not null primary key, tag int default 99);
219+
alter table t drop column tag
220+
""", 5074);
221+
Contains("dependent on column 'tag'", ex.Message);
222+
}
223+
224+
[TestMethod]
225+
public void DropColumn_WithCheckConstraint_RaisesMsg5074()
226+
{
227+
var ex = new Simulation().AssertSqlError("""
228+
create table t (id int not null primary key, qty int, constraint ck_q check (qty > 0));
229+
alter table t drop column qty
230+
""", 5074);
231+
Contains("'ck_q'", ex.Message);
232+
}
233+
234+
[TestMethod]
235+
public void DropColumn_PartOfPrimaryKey_RaisesMsg5074()
236+
=> _ = new Simulation().AssertSqlError("""
237+
create table t (id int not null primary key, val int);
238+
alter table t drop column id
239+
""", 5074);
240+
241+
[TestMethod]
242+
public void DropColumn_ReferencedByOutgoingFk_RaisesMsg5074()
243+
{
244+
var ex = new Simulation().AssertSqlError("""
245+
create table p (pid int not null primary key);
246+
create table c (id int not null primary key, p_ref int constraint fk_p references p(pid));
247+
alter table c drop column p_ref
248+
""", 5074);
249+
Contains("'fk_p'", ex.Message);
250+
}
251+
252+
[TestMethod]
253+
public void DropColumn_ReferencedByIncomingFk_RaisesMsg5074()
254+
=> _ = new Simulation().AssertSqlError("""
255+
create table p (pid int not null primary key);
256+
create table c (id int not null primary key, p_ref int references p(pid));
257+
alter table p drop column pid
258+
""", 5074);
259+
260+
[TestMethod]
261+
public void DropColumn_WithIndexKey_RaisesMsg5074()
262+
{
263+
var ex = new Simulation().AssertSqlError("""
264+
create table t (id int not null primary key, a int);
265+
create index ix_a on t(a);
266+
alter table t drop column a
267+
""", 5074);
268+
Contains("The index 'ix_a'", ex.Message);
269+
}
270+
271+
[TestMethod]
272+
public void DropColumn_WithIndexInclude_RaisesMsg5074()
273+
{
274+
var ex = new Simulation().AssertSqlError("""
275+
create table t (id int not null primary key, a int, b int);
276+
create index ix_inc on t(a) include (b);
277+
alter table t drop column b
278+
""", 5074);
279+
Contains("The index 'ix_inc'", ex.Message);
280+
}
281+
282+
[TestMethod]
283+
public void DropColumn_MultiColumn_Atomic()
284+
{
285+
var sim = new Simulation();
286+
_ = sim.ExecuteNonQuery("""
287+
create table t (id int not null primary key, a int, b int default 99);
288+
insert t values (1, 10, 100)
289+
""");
290+
// One blocker (b has DEFAULT) → both columns stay.
291+
var ex = Throws<DbException>(() => sim.ExecuteNonQuery("alter table t drop column a, b"));
292+
AreEqual("5074", ex.Data["HelpLink.EvtID"]);
293+
AreEqual(3, sim.ExecuteScalar("select count(*) from sys.columns where object_id = object_id('t')"));
294+
}
295+
296+
[TestMethod]
297+
public void DropColumn_MultiColumn_AllSucceed()
298+
{
299+
var sim = new Simulation();
300+
_ = sim.ExecuteNonQuery("""
301+
create table t (id int not null primary key, a int, b int, c int);
302+
insert t values (1, 10, 100, 1000);
303+
alter table t drop column a, c
304+
""");
305+
AreEqual(2, sim.ExecuteScalar("select count(*) from sys.columns where object_id = object_id('t')"));
306+
AreEqual(100, sim.ExecuteScalar("select b from t"));
307+
}
308+
309+
[TestMethod]
310+
public void DropColumn_IdentityColumn_Succeeds()
311+
{
312+
var sim = new Simulation();
313+
_ = sim.ExecuteNonQuery("""
314+
create table t (id int not null primary key, iden int identity(1,1));
315+
insert t (id) values (10), (20);
316+
alter table t drop column iden
317+
""");
318+
AreEqual(1, sim.ExecuteScalar("select count(*) from sys.columns where object_id = object_id('t')"));
319+
}
320+
321+
[TestMethod]
322+
public void AddThenDropColumn_RoundTripCleanlyAndPreservesRows()
323+
=> AreEqual(2, new Simulation().ExecuteScalar("""
324+
create table t (id int not null primary key);
325+
insert t values (1), (2);
326+
alter table t add note nvarchar(100);
327+
alter table t drop column note;
328+
select count(*) from t
329+
"""));
330+
331+
// --- Storage ordinal remapping ---
332+
333+
[TestMethod]
334+
public void DropMiddleColumn_RemainingColumnsReadable()
335+
{
336+
var sim = new Simulation();
337+
_ = sim.ExecuteNonQuery("""
338+
create table t (id int not null primary key, a int, b int, c int);
339+
insert t values (1, 10, 100, 1000);
340+
alter table t drop column b
341+
""");
342+
AreEqual(10, sim.ExecuteScalar("select a from t"));
343+
AreEqual(1000, sim.ExecuteScalar("select c from t"));
344+
}
345+
346+
[TestMethod]
347+
public void DropMiddleColumn_PrimaryKeyStillEnforced()
348+
{
349+
var sim = new Simulation();
350+
_ = sim.ExecuteNonQuery("""
351+
create table t (id int not null primary key, a int, b int);
352+
insert t values (1, 10, 100);
353+
alter table t drop column a
354+
""");
355+
var ex = Throws<DbException>(() => sim.ExecuteNonQuery("insert t values (1, 200)"));
356+
AreEqual("2627", ex.Data["HelpLink.EvtID"]);
357+
}
358+
359+
[TestMethod]
360+
public void DropMiddleColumn_IndexStillFunctions()
361+
{
362+
var sim = new Simulation();
363+
_ = sim.ExecuteNonQuery("""
364+
create table t (id int not null primary key, mid int, b int);
365+
create unique index ix_b on t(b);
366+
insert t values (1, 10, 100);
367+
alter table t drop column mid
368+
""");
369+
var ex = Throws<DbException>(() => sim.ExecuteNonQuery("insert t values (2, 100)"));
370+
AreEqual("2601", ex.Data["HelpLink.EvtID"]);
371+
}
372+
}

0 commit comments

Comments
 (0)