|
| 1 | +using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert; |
| 2 | + |
| 3 | +namespace SqlServerSimulator; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Behavioral tests for indexed (materialized) views: <c>CREATE UNIQUE |
| 7 | +/// CLUSTERED INDEX ON <view></c>, the schema-binding / unique-clustered |
| 8 | +/// gates (Msg 1939 / 1940 / 1941), create-time duplicate rejection (Msg 1505), |
| 9 | +/// live base-DML uniqueness enforcement (Msg 2601), the <c>sys.indexes</c> / |
| 10 | +/// <c>sys.index_columns</c> / <c>sys.stats</c> catalog surface, and |
| 11 | +/// <c>is_schema_bound</c>. Probe-confirmed against SQL Server 2025 |
| 12 | +/// (2026-07-17). |
| 13 | +/// </summary> |
| 14 | +[TestClass] |
| 15 | +public sealed class IndexedViewTests |
| 16 | +{ |
| 17 | + /// <summary> |
| 18 | + /// Base table + a schema-bound projection view + a unique clustered index |
| 19 | + /// on the view's <c>Id</c> column, with three seed rows (Ids 1/2/3). |
| 20 | + /// </summary> |
| 21 | + private static Simulation SeedIndexedView() |
| 22 | + { |
| 23 | + var sim = new Simulation(); |
| 24 | + sim.ExecuteBatches( |
| 25 | + "create table dbo.b (id int not null, grp int not null, val int not null); insert dbo.b values (1,10,100),(2,20,200),(3,30,300)", |
| 26 | + "create view dbo.v with schemabinding as select id, grp, val from dbo.b", |
| 27 | + "create unique clustered index ix_v on dbo.v(id)"); |
| 28 | + return sim; |
| 29 | + } |
| 30 | + |
| 31 | + // --- Gates --- |
| 32 | + |
| 33 | + [TestMethod] |
| 34 | + public void CreateIndex_OnNonSchemaBoundView_Msg1939() |
| 35 | + { |
| 36 | + var sim = new Simulation(); |
| 37 | + sim.ExecuteBatches( |
| 38 | + "create table dbo.b (id int not null, grp int not null)", |
| 39 | + "create view dbo.v as select id, grp from dbo.b"); |
| 40 | + var ex = sim.AssertSqlError("create unique clustered index ix on dbo.v(id)", 1939); |
| 41 | + AreEqual("Cannot create index on view 'v' because the view is not schema bound.", ex.Message); |
| 42 | + } |
| 43 | + |
| 44 | + [TestMethod] |
| 45 | + public void CreateIndex_NonUniqueClusteredOnView_Msg1941() |
| 46 | + { |
| 47 | + var sim = new Simulation(); |
| 48 | + sim.ExecuteBatches( |
| 49 | + "create table dbo.b (id int not null, grp int not null)", |
| 50 | + "create view dbo.v with schemabinding as select id, grp from dbo.b"); |
| 51 | + var ex = sim.AssertSqlError("create clustered index ix on dbo.v(id)", 1941); |
| 52 | + Contains("only unique clustered indexes are allowed", ex.Message); |
| 53 | + } |
| 54 | + |
| 55 | + [TestMethod] |
| 56 | + public void CreateIndex_NonClusteredFirstOnView_Msg1940() |
| 57 | + { |
| 58 | + var sim = new Simulation(); |
| 59 | + sim.ExecuteBatches( |
| 60 | + "create table dbo.b (id int not null, grp int not null)", |
| 61 | + "create view dbo.v with schemabinding as select id, grp from dbo.b"); |
| 62 | + var ex = sim.AssertSqlError("create nonclustered index ix on dbo.v(id)", 1940); |
| 63 | + AreEqual("Cannot create index on view 'dbo.v'. It does not have a unique clustered index.", ex.Message); |
| 64 | + } |
| 65 | + |
| 66 | + [TestMethod] |
| 67 | + public void CreateIndex_UniqueNonClusteredFirstOnView_Msg1940() |
| 68 | + { |
| 69 | + var sim = new Simulation(); |
| 70 | + sim.ExecuteBatches( |
| 71 | + "create table dbo.b (id int not null, grp int not null)", |
| 72 | + "create view dbo.v with schemabinding as select id, grp from dbo.b"); |
| 73 | + _ = sim.AssertSqlError("create unique nonclustered index ix on dbo.v(id)", 1940); |
| 74 | + } |
| 75 | + |
| 76 | + [TestMethod] |
| 77 | + public void CreateIndex_MissingViewColumn_Msg1911() |
| 78 | + { |
| 79 | + var sim = new Simulation(); |
| 80 | + sim.ExecuteBatches( |
| 81 | + "create table dbo.b (id int not null, grp int not null)", |
| 82 | + "create view dbo.v with schemabinding as select id, grp from dbo.b"); |
| 83 | + _ = sim.AssertSqlError("create unique clustered index ix on dbo.v(nope)", 1911); |
| 84 | + } |
| 85 | + |
| 86 | + // --- Create-time validation --- |
| 87 | + |
| 88 | + [TestMethod] |
| 89 | + public void CreateUniqueClusteredIndex_OverDuplicateViewRows_Msg1505() |
| 90 | + { |
| 91 | + var sim = new Simulation(); |
| 92 | + sim.ExecuteBatches( |
| 93 | + "create table dbo.b (id int not null, grp int not null); insert dbo.b values (1,10),(2,10),(3,20)", |
| 94 | + "create view dbo.v with schemabinding as select grp from dbo.b"); |
| 95 | + var ex = sim.AssertSqlError("create unique clustered index ix on dbo.v(grp)", 1505); |
| 96 | + Contains("'dbo.v'", ex.Message); |
| 97 | + Contains("(10)", ex.Message); |
| 98 | + } |
| 99 | + |
| 100 | + [TestMethod] |
| 101 | + public void CreateUniqueClusteredIndex_Succeeds() |
| 102 | + => AreEqual(3L, SeedIndexedView().ExecuteScalar("select count_big(*) from dbo.v")); |
| 103 | + |
| 104 | + // --- Enforcement --- |
| 105 | + |
| 106 | + [TestMethod] |
| 107 | + public void Insert_ProducingDuplicateViewKey_Msg2601_NamesViewAndIndex() |
| 108 | + { |
| 109 | + var ex = SeedIndexedView().AssertSqlError("insert dbo.b values (1, 99, 999)", 2601); |
| 110 | + AreEqual("Cannot insert duplicate key row in object 'dbo.v' with unique index 'ix_v'. The duplicate key value is (1).", ex.Message); |
| 111 | + } |
| 112 | + |
| 113 | + [TestMethod] |
| 114 | + public void Insert_ProducingDuplicateViewKey_RollsBackStatement() |
| 115 | + { |
| 116 | + var sim = SeedIndexedView(); |
| 117 | + _ = sim.AssertSqlError("insert dbo.b values (1, 99, 999)", 2601); |
| 118 | + AreEqual(3, sim.ExecuteScalar("select count(*) from dbo.b")); |
| 119 | + } |
| 120 | + |
| 121 | + [TestMethod] |
| 122 | + public void Update_ProducingDuplicateViewKey_Msg2601() |
| 123 | + { |
| 124 | + var sim = SeedIndexedView(); |
| 125 | + var ex = sim.AssertSqlError("update dbo.b set id = 2 where id = 3", 2601); |
| 126 | + Contains("'dbo.v'", ex.Message); |
| 127 | + AreEqual(3, sim.ExecuteScalar("select count(*) from dbo.b where id in (1,2,3)")); |
| 128 | + } |
| 129 | + |
| 130 | + [TestMethod] |
| 131 | + public void Insert_NonDuplicate_Succeeds() |
| 132 | + { |
| 133 | + var sim = SeedIndexedView(); |
| 134 | + _ = sim.ExecuteNonQuery("insert dbo.b values (4, 40, 400)"); |
| 135 | + AreEqual(4, sim.ExecuteScalar("select count(*) from dbo.b")); |
| 136 | + } |
| 137 | + |
| 138 | + [TestMethod] |
| 139 | + public void Delete_NeverViolatesViewUniqueness() |
| 140 | + { |
| 141 | + var sim = SeedIndexedView(); |
| 142 | + _ = sim.ExecuteNonQuery("delete dbo.b where id = 3"); |
| 143 | + AreEqual(2, sim.ExecuteScalar("select count(*) from dbo.b")); |
| 144 | + } |
| 145 | + |
| 146 | + // --- is_schema_bound --- |
| 147 | + |
| 148 | + [TestMethod] |
| 149 | + public void SchemaBinding_SurfacesThroughSqlModulesAndObjectProperty() |
| 150 | + { |
| 151 | + var sim = SeedIndexedView(); |
| 152 | + IsTrue((bool)sim.ExecuteScalar("select is_schema_bound from sys.sql_modules where object_id = object_id('dbo.v')")!); |
| 153 | + AreEqual(1, sim.ExecuteScalar("select objectproperty(object_id('dbo.v'), 'IsSchemaBound')")); |
| 154 | + } |
| 155 | + |
| 156 | + [TestMethod] |
| 157 | + public void NonSchemaBoundView_ReportsIsSchemaBoundFalse() |
| 158 | + { |
| 159 | + var sim = new Simulation(); |
| 160 | + sim.ExecuteBatches( |
| 161 | + "create table dbo.b (id int not null)", |
| 162 | + "create view dbo.v as select id from dbo.b"); |
| 163 | + IsFalse((bool)sim.ExecuteScalar("select is_schema_bound from sys.sql_modules where object_id = object_id('dbo.v')")!); |
| 164 | + } |
| 165 | + |
| 166 | + // --- Catalog surface --- |
| 167 | + |
| 168 | + [TestMethod] |
| 169 | + public void ViewIndex_SurfacesInSysIndexes_AsClusteredUnique() |
| 170 | + { |
| 171 | + var sim = SeedIndexedView(); |
| 172 | + AreEqual(1, sim.ExecuteScalar( |
| 173 | + "select index_id from sys.indexes where object_id = object_id('dbo.v') and name = 'ix_v' and type_desc = 'CLUSTERED' and is_unique = 1 and is_primary_key = 0 and is_unique_constraint = 0")); |
| 174 | + // No HEAP row for a view (no index_id 0). |
| 175 | + AreEqual(0, sim.ExecuteScalar("select count(*) from sys.indexes where object_id = object_id('dbo.v') and index_id = 0")); |
| 176 | + } |
| 177 | + |
| 178 | + [TestMethod] |
| 179 | + public void PlainView_HasNoSysIndexesRows() |
| 180 | + { |
| 181 | + var sim = new Simulation(); |
| 182 | + sim.ExecuteBatches( |
| 183 | + "create table dbo.b (id int not null)", |
| 184 | + "create view dbo.v as select id from dbo.b"); |
| 185 | + AreEqual(0, sim.ExecuteScalar("select count(*) from sys.indexes where object_id = object_id('dbo.v')")); |
| 186 | + } |
| 187 | + |
| 188 | + [TestMethod] |
| 189 | + public void ViewIndex_SurfacesInSysIndexColumns_WithViewColumnId() |
| 190 | + { |
| 191 | + // Two key columns: id (column_id 1) and grp (column_id 2). |
| 192 | + var sim = new Simulation(); |
| 193 | + sim.ExecuteBatches( |
| 194 | + "create table dbo.b (id int not null, grp int not null, val int not null); insert dbo.b values (1,10,100)", |
| 195 | + "create view dbo.v with schemabinding as select id, grp, val from dbo.b", |
| 196 | + "create unique clustered index ix_v on dbo.v(id, grp)"); |
| 197 | + AreEqual(2, sim.ExecuteScalar("select count(*) from sys.index_columns where object_id = object_id('dbo.v') and is_included_column = 0")); |
| 198 | + AreEqual(1, sim.ExecuteScalar("select column_id from sys.index_columns where object_id = object_id('dbo.v') and key_ordinal = 1")); |
| 199 | + AreEqual(2, sim.ExecuteScalar("select column_id from sys.index_columns where object_id = object_id('dbo.v') and key_ordinal = 2")); |
| 200 | + } |
| 201 | + |
| 202 | + [TestMethod] |
| 203 | + public void ViewIndex_SurfacesInSysStats() |
| 204 | + { |
| 205 | + var sim = SeedIndexedView(); |
| 206 | + AreEqual(1, sim.ExecuteScalar("select stats_id from sys.stats where object_id = object_id('dbo.v') and name = 'ix_v'")); |
| 207 | + AreEqual(1, sim.ExecuteScalar("select count(*) from sys.stats_columns where object_id = object_id('dbo.v') and stats_id = 1")); |
| 208 | + } |
| 209 | + |
| 210 | + // --- NOEXPAND --- |
| 211 | + |
| 212 | + [TestMethod] |
| 213 | + public void Select_FromIndexedView_WithNoExpand_Accepted() |
| 214 | + => AreEqual(3L, SeedIndexedView().ExecuteScalar("select count_big(*) from dbo.v with (noexpand)")); |
| 215 | +} |
0 commit comments