Skip to content

Commit c44ee5b

Browse files
Merge pull request #731 from erikdarlingdata/fix/indexcleanup-expanded-tests
sp_IndexCleanup: expand adversarial tests to 26 assertions + heap fix
2 parents ae10a13 + 6f376dc commit c44ee5b

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

13 KB
Binary file not shown.

sp_IndexCleanup/tests/adversarial_test.sql

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ DROP TABLE IF EXISTS dbo.test_ic_filtered;
3434
DROP TABLE IF EXISTS dbo.test_ic_heap;
3535
DROP TABLE IF EXISTS dbo.test_ic_multi;
3636
DROP TABLE IF EXISTS dbo.test_ic_view_base;
37+
DROP TABLE IF EXISTS dbo.test_ic_exact;
38+
DROP TABLE IF EXISTS dbo.test_ic_reverse;
39+
DROP TABLE IF EXISTS dbo.test_ic_filter_eq;
40+
DROP TABLE IF EXISTS dbo.test_ic_uc_replace;
41+
DROP TABLE IF EXISTS dbo.test_ic_interact;
3742
GO
3843

3944
/* ============================================= */
@@ -83,6 +88,49 @@ CREATE TABLE dbo.test_ic_multi
8388
col_b integer NOT NULL
8489
);
8590

91+
CREATE TABLE dbo.test_ic_exact
92+
(
93+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
94+
col_a integer NOT NULL,
95+
col_b integer NOT NULL,
96+
col_c integer NOT NULL
97+
);
98+
99+
CREATE TABLE dbo.test_ic_reverse
100+
(
101+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
102+
col_a integer NOT NULL,
103+
col_b integer NOT NULL,
104+
col_c integer NOT NULL
105+
);
106+
107+
CREATE TABLE dbo.test_ic_filter_eq
108+
(
109+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
110+
col_a integer NOT NULL,
111+
col_b integer NOT NULL,
112+
status_code integer NOT NULL
113+
);
114+
115+
CREATE TABLE dbo.test_ic_uc_replace
116+
(
117+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
118+
col_a integer NOT NULL,
119+
col_b integer NOT NULL,
120+
col_c integer NOT NULL,
121+
col_d integer NOT NULL
122+
);
123+
124+
CREATE TABLE dbo.test_ic_interact
125+
(
126+
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
127+
col_a integer NOT NULL,
128+
col_b integer NOT NULL,
129+
col_c integer NOT NULL,
130+
col_d integer NOT NULL,
131+
col_e nvarchar(100) NULL
132+
);
133+
86134
CREATE TABLE dbo.test_ic_view_base
87135
(
88136
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
@@ -132,6 +180,32 @@ INSERT INTO dbo.test_ic_view_base (col_a, col_b, col_c)
132180
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 100, ABS(CHECKSUM(NEWID())) % 50,
133181
ABS(CHECKSUM(NEWID())) % 200
134182
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
183+
184+
INSERT INTO dbo.test_ic_exact (col_a, col_b, col_c)
185+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
186+
ABS(CHECKSUM(NEWID())) % 200
187+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
188+
189+
INSERT INTO dbo.test_ic_reverse (col_a, col_b, col_c)
190+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
191+
ABS(CHECKSUM(NEWID())) % 200
192+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
193+
194+
INSERT INTO dbo.test_ic_filter_eq (col_a, col_b, status_code)
195+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
196+
ABS(CHECKSUM(NEWID())) % 5
197+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
198+
199+
INSERT INTO dbo.test_ic_uc_replace (col_a, col_b, col_c, col_d)
200+
SELECT TOP (10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
201+
ABS(CHECKSUM(NEWID())) % 500, ABS(CHECKSUM(NEWID())) % 200,
202+
ABS(CHECKSUM(NEWID())) % 100
203+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
204+
205+
INSERT INTO dbo.test_ic_interact (col_a, col_b, col_c, col_d, col_e)
206+
SELECT TOP (10000) ABS(CHECKSUM(NEWID())) % 1000, ABS(CHECKSUM(NEWID())) % 500,
207+
ABS(CHECKSUM(NEWID())) % 200, ABS(CHECKSUM(NEWID())) % 100, LEFT(NEWID(), 20)
208+
FROM sys.all_objects AS a CROSS JOIN sys.all_objects AS b;
135209
GO
136210

137211
/* ============================================= */
@@ -183,6 +257,36 @@ CREATE NONCLUSTERED INDEX ix_heap_a_dup ON dbo.test_ic_heap (col_a);
183257
/* Group 7: Multi-table isolation */
184258
CREATE INDEX ix_multi_a ON dbo.test_ic_multi (col_a);
185259
CREATE INDEX ix_basic_col_d ON dbo.test_ic_basic (col_d);
260+
261+
/* Group 8: Exact Duplicate — same keys AND same includes */
262+
CREATE INDEX ix_exact_ab_1 ON dbo.test_ic_exact (col_a, col_b) INCLUDE (col_c);
263+
CREATE INDEX ix_exact_ab_2 ON dbo.test_ic_exact (col_a, col_b) INCLUDE (col_c);
264+
265+
/* Group 9: Reverse Duplicate — same columns, different leading order */
266+
CREATE INDEX ix_rev_ab ON dbo.test_ic_reverse (col_a, col_b);
267+
CREATE INDEX ix_rev_ba ON dbo.test_ic_reverse (col_b, col_a);
268+
269+
/* Group 10: Equal Except For Filter */
270+
/* 10a: Same keys, one filtered one not — should NOT match */
271+
CREATE INDEX ix_feq_a ON dbo.test_ic_filter_eq (col_a);
272+
CREATE INDEX ix_feq_a_filt ON dbo.test_ic_filter_eq (col_a) WHERE status_code = 1;
273+
274+
/* Group 11: UC Replacement (Rule 7/7.5) — exact key match */
275+
ALTER TABLE dbo.test_ic_uc_replace ADD CONSTRAINT uq_ucr_ab UNIQUE (col_a, col_b);
276+
CREATE NONCLUSTERED INDEX ix_ucr_ab_inc ON dbo.test_ic_uc_replace (col_a, col_b) INCLUDE (col_c);
277+
278+
/* Group 12: Rule interactions */
279+
/* 12a: Multi-level subset: A ⊂ AB ⊂ ABC */
280+
CREATE INDEX ix_int_a ON dbo.test_ic_interact (col_a);
281+
CREATE INDEX ix_int_ab ON dbo.test_ic_interact (col_a, col_b);
282+
CREATE INDEX ix_int_abc ON dbo.test_ic_interact (col_a, col_b, col_c);
283+
284+
/* 12b: UC exact match AND UC superset on same table */
285+
ALTER TABLE dbo.test_ic_interact ADD CONSTRAINT uq_int_cd UNIQUE (col_c, col_d);
286+
CREATE INDEX ix_int_cd ON dbo.test_ic_interact (col_c, col_d) INCLUDE (col_e);
287+
CREATE INDEX ix_int_c ON dbo.test_ic_interact (col_c);
288+
289+
/* Group 13: @min_reads filter — run separately in Python */
186290
GO
187291

188292
/* ============================================= */
@@ -220,6 +324,25 @@ BEGIN
220324
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_heap WITH (INDEX = ix_heap_a_dup) WHERE col_a = 2;
221325
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_multi WITH (INDEX = ix_multi_a) WHERE col_a = 1;
222326
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_basic WITH (INDEX = ix_basic_col_d) WHERE col_d = 1;
327+
/* Group 8: Exact duplicates */
328+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_exact WITH (INDEX = ix_exact_ab_1) WHERE col_a = 1;
329+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_exact WITH (INDEX = ix_exact_ab_2) WHERE col_a = 2;
330+
/* Group 9: Reverse duplicates */
331+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_reverse WITH (INDEX = ix_rev_ab) WHERE col_a = 1;
332+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_reverse WITH (INDEX = ix_rev_ba) WHERE col_b = 1;
333+
/* Group 10: Equal except filter */
334+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filter_eq WITH (INDEX = ix_feq_a) WHERE col_a = 1;
335+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_filter_eq WITH (INDEX = ix_feq_a_filt) WHERE col_a = 1 AND status_code = 1;
336+
/* Group 11: UC replacement */
337+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc_replace WITH (INDEX = uq_ucr_ab) WHERE col_a = 1;
338+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_uc_replace WITH (INDEX = ix_ucr_ab_inc) WHERE col_a = 1;
339+
/* Group 12: Interactions */
340+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_interact WITH (INDEX = ix_int_a) WHERE col_a = 1;
341+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_interact WITH (INDEX = ix_int_ab) WHERE col_a = 1;
342+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_interact WITH (INDEX = ix_int_abc) WHERE col_a = 1;
343+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_interact WITH (INDEX = uq_int_cd) WHERE col_c = 1;
344+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_interact WITH (INDEX = ix_int_cd) WHERE col_c = 1;
345+
SELECT @c = COUNT_BIG(*) FROM dbo.test_ic_interact WITH (INDEX = ix_int_c) WHERE col_c = 1;
223346
SELECT @i += 1;
224347
END;
225348
GO
@@ -250,4 +373,9 @@ DROP TABLE IF EXISTS dbo.test_ic_filtered;
250373
DROP TABLE IF EXISTS dbo.test_ic_heap;
251374
DROP TABLE IF EXISTS dbo.test_ic_multi;
252375
DROP TABLE IF EXISTS dbo.test_ic_view_base;
376+
DROP TABLE IF EXISTS dbo.test_ic_exact;
377+
DROP TABLE IF EXISTS dbo.test_ic_reverse;
378+
DROP TABLE IF EXISTS dbo.test_ic_filter_eq;
379+
DROP TABLE IF EXISTS dbo.test_ic_uc_replace;
380+
DROP TABLE IF EXISTS dbo.test_ic_interact;
253381
GO

sp_IndexCleanup/tests/run_tests.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,74 @@ def assert_test(group, name, condition, detail=""):
215215
assert_test("7-Isolation", "7a: Cross-table NOT flagged as duplicate",
216216
len(matches) == 0, f"found {len(matches)} (expected 0)")
217217

218+
# ---- Group 8: Exact Duplicate ----
219+
220+
# 8a: Same keys AND same includes → one DISABLE
221+
matches = find_rows(rows, table_name="test_ic_exact",
222+
index_name__in={"ix_exact_ab_1", "ix_exact_ab_2"},
223+
script_type="DISABLE SCRIPT")
224+
assert_test("8-Exact-Dup", "8a: Exact duplicate flagged DISABLE",
225+
len(matches) >= 1, f"found {len(matches)}")
226+
227+
# ---- Group 9: Reverse Duplicate ----
228+
229+
# 9a: Different leading column order → NOT flagged (by design — different query patterns)
230+
matches = find_rows(rows, table_name="test_ic_reverse",
231+
index_name__in={"ix_rev_ab", "ix_rev_ba"},
232+
script_type="DISABLE SCRIPT")
233+
assert_test("9-Reverse", "9a: Different leading col NOT flagged DISABLE (by design)",
234+
len(matches) == 0, f"found {len(matches)} (expected 0)")
235+
236+
# ---- Group 10: Equal Except For Filter ----
237+
238+
# 10a: Same keys, one filtered one not → should NOT be duplicates
239+
matches = find_rows(rows, table_name="test_ic_filter_eq", index_name="ix_feq_a_filt",
240+
script_type="DISABLE SCRIPT", additional_info__like="Duplicate")
241+
assert_test("10-FilterEq", "10a: Filtered vs unfiltered NOT flagged duplicate",
242+
len(matches) == 0, f"found {len(matches)} (expected 0)")
243+
244+
# ---- Group 11: UC Replacement (Rule 7/7.5) ----
245+
246+
# 11a: UC exact match with NC that has includes → UC gets DROP CONSTRAINT
247+
matches = find_rows(rows, table_name="test_ic_uc_replace", index_name="uq_ucr_ab",
248+
script_type="DISABLE CONSTRAINT SCRIPT")
249+
assert_test("11-UC-Replace", "11a: UC with exact-match NC gets DROP CONSTRAINT",
250+
len(matches) == 1, f"found {len(matches)}")
251+
252+
# 11b: NC with includes gets MAKE UNIQUE (MERGE SCRIPT with CREATE UNIQUE)
253+
matches = find_rows(rows, table_name="test_ic_uc_replace", index_name="ix_ucr_ab_inc",
254+
script_type="MERGE SCRIPT")
255+
has_unique = any("CREATE UNIQUE" in m.get("script", "") for m in matches)
256+
assert_test("11-UC-Replace", "11b: NC replacement has CREATE UNIQUE",
257+
has_unique, f"found {len(matches)} merge rows, unique={has_unique}")
258+
259+
# ---- Group 12: Rule interactions ----
260+
261+
# 12a: Multi-level subset: ix_int_a ⊂ ix_int_ab ⊂ ix_int_abc
262+
# Narrowest (ix_int_a) should be DISABLE
263+
matches = find_rows(rows, table_name="test_ic_interact", index_name="ix_int_a",
264+
script_type="DISABLE SCRIPT")
265+
assert_test("12-Interact", "12a: Narrowest subset (A) flagged DISABLE",
266+
len(matches) == 1, f"found {len(matches)}")
267+
268+
# Middle (ix_int_ab) should also be DISABLE
269+
matches = find_rows(rows, table_name="test_ic_interact", index_name="ix_int_ab",
270+
script_type="DISABLE SCRIPT")
271+
assert_test("12-Interact", "12a: Middle subset (AB) flagged DISABLE",
272+
len(matches) == 1, f"found {len(matches)}")
273+
274+
# Widest (ix_int_abc) should survive (MERGE or COMPRESSION, not DISABLE)
275+
matches = find_rows(rows, table_name="test_ic_interact", index_name="ix_int_abc",
276+
script_type="DISABLE SCRIPT")
277+
assert_test("12-Interact", "12a: Widest (ABC) NOT disabled",
278+
len(matches) == 0, f"found {len(matches)} (expected 0)")
279+
280+
# 12b: UC + NC + subset on same table
281+
# KNOWN ISSUE: uq_int_cd, ix_int_cd, and ix_int_c don't appear in
282+
# output at all — needs investigation with @debug = 1 to determine
283+
# if they're excluded at collection or rule processing stage.
284+
# Skipping assertion for now — tracked as issue for investigation.
285+
218286
return results
219287

220288

0 commit comments

Comments
 (0)