Skip to content

Commit 2a5a894

Browse files
committed
Catalog-view predicate pushdown: a top-level AND-conjunct equality on object_id (major_id for extended properties) with a row-independent comparand routes sys.columns/all_columns, sys.indexes, sys.index_columns, sys.parameters/all_parameters, and sys.extended_properties through a filtered row generator that enumerates only the matching object; the full WHERE stays as residual filter so pushdown can only over-produce, the comparand resolves per execution for plan-cache safety, non-integer or out-of-int-range comparands fall back to residual/empty without error.
1 parent 9fab873 commit 2a5a894

21 files changed

Lines changed: 862 additions & 28 deletions
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
using SqlServerSimulator.Parser;
2+
using static Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
3+
4+
namespace SqlServerSimulator;
5+
6+
/// <summary>
7+
/// Guards the catalog-view predicate pushdown (<c>Selection.BuildSqlProjection</c>
8+
/// → <c>Selection.ForCatalogView</c>): a leftmost pushdown-aware catalog view
9+
/// (<c>sys.columns</c> and peers) with a top-level <c>&lt;key&gt; = &lt;row-independent
10+
/// value&gt;</c> WHERE conjunct must hand the key into the row generator so it
11+
/// enumerates only the matching object (the <c>Seek(view.column)</c> trace), a
12+
/// NULL comparand must short-circuit to no rows (<c>SeekEmpty</c>), and shapes
13+
/// that can't push (OR-combined predicate, catalog view not leftmost, no
14+
/// eligible conjunct) must run the full generator (<c>Scan</c>). The pushdown is
15+
/// result-transparent — the full WHERE re-applies as a residual filter — so the
16+
/// correctness suite passes either way; these read the opt-in
17+
/// <see cref="CatalogPushdownDiagnostics"/> trace to assert the path directly and
18+
/// confirm the rows stay correct under it.
19+
/// </summary>
20+
[TestClass]
21+
public sealed class CatalogPushdownTests
22+
{
23+
public TestContext TestContext { get; set; } = null!;
24+
25+
// Runs `setup` then `query` on one connection, capturing the pushdown trace
26+
// and each result row's first column.
27+
private static (List<string> Trace, List<object?> Rows) Run(string query, string? setup = null)
28+
{
29+
var connection = new Simulation().CreateDbConnection();
30+
connection.Open();
31+
using (var s = connection.CreateCommand())
32+
{
33+
s.CommandText = setup ?? """
34+
create table t1 (a int not null primary key, b nvarchar(20) null);
35+
create table t2 (c int not null primary key, d date null, e money null);
36+
create table t3 (f bigint not null primary key);
37+
""";
38+
_ = s.ExecuteNonQuery();
39+
}
40+
41+
CatalogPushdownDiagnostics.Sink = [];
42+
try
43+
{
44+
using var command = connection.CreateCommand();
45+
command.CommandText = query;
46+
using var reader = command.ExecuteReader();
47+
var rows = new List<object?>();
48+
while (reader.Read())
49+
rows.Add(reader.IsDBNull(0) ? null : reader.GetValue(0));
50+
return (CatalogPushdownDiagnostics.Sink, rows);
51+
}
52+
finally
53+
{
54+
CatalogPushdownDiagnostics.Sink = null;
55+
}
56+
}
57+
58+
[TestMethod]
59+
public void ColumnsByObjectIdFunction_Seeks()
60+
{
61+
var (trace, rows) = Run("select name from sys.columns c where c.object_id = object_id('dbo.t2') order by column_id");
62+
Contains("Seek(columns.object_id)", trace);
63+
DoesNotContain("Scan(columns)", trace);
64+
HasCount(3, rows);
65+
AreEqual("c", rows[0]);
66+
AreEqual("d", rows[1]);
67+
AreEqual("e", rows[2]);
68+
}
69+
70+
[TestMethod]
71+
public void AllColumnsByObjectId_Seeks()
72+
{
73+
var (trace, rows) = Run("select name from sys.all_columns where object_id = object_id('dbo.t2') order by column_id");
74+
Contains("Seek(all_columns.object_id)", trace);
75+
HasCount(3, rows);
76+
}
77+
78+
[TestMethod]
79+
public void ColumnsByLiteralObjectId_Seeks()
80+
{
81+
var connection = new Simulation().CreateDbConnection();
82+
connection.Open();
83+
using (var s = connection.CreateCommand())
84+
{
85+
s.CommandText = "create table t2 (c int not null primary key, d date null, e money null)";
86+
_ = s.ExecuteNonQuery();
87+
}
88+
int id;
89+
using (var idc = connection.CreateCommand())
90+
{
91+
idc.CommandText = "select object_id('dbo.t2')";
92+
id = (int)idc.ExecuteScalar()!;
93+
}
94+
95+
CatalogPushdownDiagnostics.Sink = [];
96+
try
97+
{
98+
using var command = connection.CreateCommand();
99+
command.CommandText = $"select count(*) from sys.columns where object_id = {id}";
100+
using var reader = command.ExecuteReader();
101+
_ = reader.Read();
102+
AreEqual(3, reader.GetInt32(0));
103+
Contains("Seek(columns.object_id)", CatalogPushdownDiagnostics.Sink!);
104+
}
105+
finally
106+
{
107+
CatalogPushdownDiagnostics.Sink = null;
108+
}
109+
}
110+
111+
[TestMethod]
112+
public void ColumnsByVariable_Seeks()
113+
{
114+
var (trace, rows) = Run("""
115+
declare @id int = object_id('dbo.t3');
116+
select name from sys.columns where object_id = @id order by column_id
117+
""");
118+
Contains("Seek(columns.object_id)", trace);
119+
HasCount(1, rows);
120+
AreEqual("f", rows[0]);
121+
}
122+
123+
[TestMethod]
124+
public void ColumnsPredicateAndedWithOther_Seeks()
125+
{
126+
var (trace, rows) = Run("select name from sys.columns c where c.object_id = object_id('dbo.t2') and c.is_nullable = 1 order by column_id");
127+
Contains("Seek(columns.object_id)", trace);
128+
// Residual WHERE still drops the non-nullable key column.
129+
HasCount(2, rows);
130+
AreEqual("d", rows[0]);
131+
AreEqual("e", rows[1]);
132+
}
133+
134+
[TestMethod]
135+
public void ColumnsOrPredicate_DoesNotPush()
136+
{
137+
var (trace, rows) = Run("select name from sys.columns c where c.object_id = object_id('dbo.t2') or c.object_id = object_id('dbo.t3') order by object_id, column_id");
138+
Contains("Scan(columns)", trace);
139+
DoesNotContain("Seek(columns.object_id)", trace);
140+
// Union of t2 (c, d, e) and t3 (f) columns.
141+
HasCount(4, rows);
142+
}
143+
144+
[TestMethod]
145+
public void CatalogViewNotLeftmost_DoesNotPush()
146+
{
147+
// Leftmost source is the base table t2; sys.columns is the join's right
148+
// side, so no pushdown fires — but results stay correct.
149+
var (trace, rows) = Run("""
150+
select c.name from t2 join sys.columns c on c.object_id = object_id('dbo.t2')
151+
where t2.c is not null order by c.column_id
152+
""");
153+
DoesNotContain("Seek(columns.object_id)", trace);
154+
// t2 has no rows, so the inner join yields nothing regardless.
155+
IsEmpty(rows);
156+
}
157+
158+
[TestMethod]
159+
public void UnknownObject_SeeksEmpty()
160+
{
161+
var (trace, rows) = Run("select name from sys.columns where object_id = object_id('dbo.does_not_exist')");
162+
Contains("SeekEmpty(columns.object_id)", trace);
163+
IsEmpty(rows);
164+
}
165+
166+
[TestMethod]
167+
public void ExplicitNullComparand_SeeksEmpty()
168+
{
169+
var (trace, rows) = Run("select name from sys.columns where object_id = null");
170+
Contains("SeekEmpty(columns.object_id)", trace);
171+
IsEmpty(rows);
172+
}
173+
174+
[TestMethod]
175+
public void IndexesByObjectId_Seeks()
176+
{
177+
var (trace, rows) = Run("select name from sys.indexes where object_id = object_id('dbo.t1')");
178+
Contains("Seek(indexes.object_id)", trace);
179+
// t1's clustered PK index.
180+
HasCount(1, rows);
181+
}
182+
183+
[TestMethod]
184+
public void IndexColumnsByObjectId_Seeks()
185+
{
186+
var (trace, rows) = Run("select index_id from sys.index_columns where object_id = object_id('dbo.t1')");
187+
Contains("Seek(index_columns.object_id)", trace);
188+
HasCount(1, rows);
189+
}
190+
191+
[TestMethod]
192+
public void ParametersByObjectId_Seeks()
193+
{
194+
var (trace, rows) = Run(
195+
"select name from sys.parameters where object_id = object_id('dbo.p') order by parameter_id",
196+
setup: "create procedure p @x int, @y nvarchar(10) as select 1");
197+
Contains("Seek(parameters.object_id)", trace);
198+
HasCount(2, rows);
199+
AreEqual("@x", rows[0]);
200+
AreEqual("@y", rows[1]);
201+
}
202+
203+
[TestMethod]
204+
public void ExtendedPropertiesByMajorId_Seeks()
205+
{
206+
var (trace, rows) = Run(
207+
"select cast(value as nvarchar(100)) from sys.extended_properties where major_id = object_id('dbo.t1')",
208+
setup: """
209+
create table t1 (a int);
210+
create table t2 (b int);
211+
exec sp_addextendedproperty @name = N'MS_Description', @value = N'one', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N't1';
212+
exec sp_addextendedproperty @name = N'MS_Description', @value = N'two', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N't2';
213+
""");
214+
Contains("Seek(extended_properties.major_id)", trace);
215+
HasCount(1, rows);
216+
AreEqual("one", rows[0]);
217+
}
218+
219+
[TestMethod]
220+
public void NoEligibleConjunct_Scans()
221+
{
222+
var (trace, _) = Run("select name from sys.columns where name = 'a'");
223+
Contains("Scan(columns)", trace);
224+
DoesNotContain("Seek(columns.object_id)", trace);
225+
}
226+
}

0 commit comments

Comments
 (0)