@@ -81,78 +81,37 @@ def test_select_with_delete_in_where_not_blocked(self):
8181
8282
8383# ===================================================================
84- # 2. Auto-inject TOP when missing
84+ # 2. Server enforces TOP 5000 (no client-side injection needed)
8585# ===================================================================
8686
8787
88- class TestAutoInjectTop :
89- """Queries without TOP or OFFSET should get TOP 5000 injected ."""
88+ class TestNoTopInjection :
89+ """Verify the SDK does NOT inject TOP -- server handles the 5000 cap ."""
9090
91- def test_no_top_injects_top_5000 (self ):
91+ def test_no_top_passes_through_unchanged (self ):
9292 c = _client ()
93- with warnings .catch_warnings (record = True ) as w :
94- warnings .simplefilter ("always" )
95- result = c ._sql_guardrails ("SELECT name FROM account" )
96- assert len (w ) >= 1
97- assert "TOP 5000" in str (w [0 ].message )
98- assert "TOP 5000" in result
93+ sql = "SELECT name FROM account"
94+ result = c ._sql_guardrails (sql )
95+ assert result == sql
96+ assert "TOP" not in result
9997
10098 def test_existing_top_not_modified (self ):
10199 c = _client ()
102- with warnings .catch_warnings (record = True ) as w :
103- warnings .simplefilter ("always" )
104- result = c ._sql_guardrails ("SELECT TOP 100 name FROM account" )
105- top_warnings = [x for x in w if "TOP" in str (x .message )]
106- assert len (top_warnings ) == 0
100+ result = c ._sql_guardrails ("SELECT TOP 100 name FROM account" )
107101 assert "TOP 100" in result
108- assert "TOP 5000" not in result
109-
110- def test_existing_offset_not_modified (self ):
111- c = _client ()
112- with warnings .catch_warnings (record = True ) as w :
113- warnings .simplefilter ("always" )
114- result = c ._sql_guardrails (
115- "SELECT name FROM account ORDER BY name " "OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY"
116- )
117- top_warnings = [x for x in w if "TOP" in str (x .message )]
118- assert len (top_warnings ) == 0
119- assert "TOP 5000" not in result
120102
121- def test_distinct_gets_top_after_select_distinct (self ):
103+ def test_offset_passes_through (self ):
122104 c = _client ()
123- with warnings .catch_warnings (record = True ):
124- warnings .simplefilter ("always" )
125- result = c ._sql_guardrails ("SELECT DISTINCT name FROM account" )
126- assert "SELECT DISTINCT TOP 5000" in result or "SELECT DISTINCT TOP 5000" in result .upper ()
105+ sql = "SELECT name FROM account ORDER BY name OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY"
106+ result = c ._sql_guardrails (sql )
107+ assert result == sql
127108
128- def test_count_star_gets_top (self ):
109+ def test_join_without_top_not_modified (self ):
129110 c = _client ()
130- with warnings .catch_warnings (record = True ) as w :
131- warnings .simplefilter ("always" )
132- result = c ._sql_guardrails ("SELECT COUNT(*) FROM account" )
133- assert any ("TOP 5000" in str (x .message ) for x in w )
134- assert "TOP 5000" in result
135-
136- def test_join_without_top_gets_top (self ):
137- c = _client ()
138- with warnings .catch_warnings (record = True ) as w :
139- warnings .simplefilter ("always" )
140- result = c ._sql_guardrails (
141- "SELECT a.name, c.fullname FROM account a " "JOIN contact c ON a.accountid = c.parentcustomerid"
142- )
143- assert any ("TOP 5000" in str (x .message ) for x in w )
144- assert "TOP 5000" in result
145-
146- def test_join_with_top_not_modified (self ):
147- c = _client ()
148- with warnings .catch_warnings (record = True ) as w :
149- warnings .simplefilter ("always" )
150- result = c ._sql_guardrails (
151- "SELECT TOP 10 a.name FROM account a " "JOIN contact c ON a.accountid = c.parentcustomerid"
152- )
153- top_warnings = [x for x in w if "TOP" in str (x .message )]
154- assert len (top_warnings ) == 0
155- assert "TOP 10" in result
111+ sql = "SELECT a.name, c.fullname FROM account a " "JOIN contact c ON a.accountid = c.parentcustomerid"
112+ result = c ._sql_guardrails (sql )
113+ assert result == sql
114+ assert "TOP" not in result
156115
157116
158117# ===================================================================
@@ -232,7 +191,52 @@ def test_single_table_no_warning(self):
232191
233192
234193# ===================================================================
235- # 5. Integration: _query_sql applies guardrails
194+ # 5. SELECT * with JOIN warning (from _expand_select_star)
195+ # ===================================================================
196+
197+
198+ class TestSelectStarJoinWarning :
199+ """SELECT * with JOIN should warn that only first table columns are used."""
200+
201+ def test_select_star_with_join_warns (self ):
202+ c = _client ()
203+ c ._list_columns = MagicMock (return_value = [{"LogicalName" : "name" }, {"LogicalName" : "accountid" }])
204+ with warnings .catch_warnings (record = True ) as w :
205+ warnings .simplefilter ("always" )
206+ c ._expand_select_star (
207+ "SELECT * FROM account a JOIN contact c ON a.accountid = c.parentcustomerid" ,
208+ "account" ,
209+ )
210+ join_warnings = [x for x in w if "JOIN" in str (x .message )]
211+ assert len (join_warnings ) == 1
212+ assert "first table only" in str (join_warnings [0 ].message )
213+
214+ def test_select_star_no_join_no_warning (self ):
215+ c = _client ()
216+ c ._list_columns = MagicMock (return_value = [{"LogicalName" : "name" }])
217+ with warnings .catch_warnings (record = True ) as w :
218+ warnings .simplefilter ("always" )
219+ c ._expand_select_star ("SELECT * FROM account" , "account" )
220+ join_warnings = [x for x in w if "JOIN" in str (x .message )]
221+ assert len (join_warnings ) == 0
222+
223+ def test_no_star_with_join_no_warning (self ):
224+ c = _client ()
225+ c ._list_columns = MagicMock ()
226+ with warnings .catch_warnings (record = True ) as w :
227+ warnings .simplefilter ("always" )
228+ c ._expand_select_star (
229+ "SELECT a.name, c.fullname FROM account a " "JOIN contact c ON a.accountid = c.parentcustomerid" ,
230+ "account" ,
231+ )
232+ # _list_columns not called (no star), so no JOIN warning
233+ c ._list_columns .assert_not_called ()
234+ join_warnings = [x for x in w if "JOIN" in str (x .message )]
235+ assert len (join_warnings ) == 0
236+
237+
238+ # ===================================================================
239+ # 6. Integration: _query_sql applies guardrails
236240# ===================================================================
237241
238242
@@ -246,22 +250,23 @@ def test_write_blocked_before_server_call(self):
246250 c ._query_sql ("DELETE FROM account WHERE name = 'x'" )
247251 c ._request .assert_not_called ()
248252
249- def test_top_injected_in_server_request (self ):
253+ def test_no_top_injection_in_server_request (self ):
254+ """Server manages the 5000 cap -- SDK should not inject TOP."""
250255 c = _client ()
251256 c ._entity_set_from_schema_name = MagicMock (return_value = "accounts" )
252257 mock_response = MagicMock ()
253258 mock_response .json .return_value = {"value" : []}
254259 mock_response .status_code = 200
255260 c ._request = MagicMock (return_value = mock_response )
256261
257- with warnings .catch_warnings (record = True ):
258- warnings .simplefilter ("always" )
259- c ._query_sql ("SELECT name FROM account" )
262+ c ._query_sql ("SELECT name FROM account" )
260263
261264 call_args = c ._request .call_args
262265 sent_params = call_args [1 ].get ("params" , {})
263266 sent_sql = sent_params .get ("sql" , "" )
264- assert "TOP 5000" in sent_sql
267+ # SDK should NOT inject TOP 5000
268+ assert "TOP 5000" not in sent_sql
269+ assert sent_sql == "SELECT name FROM account"
265270
266271 def test_explicit_top_preserved_in_server_request (self ):
267272 c = _client ()
@@ -277,4 +282,3 @@ def test_explicit_top_preserved_in_server_request(self):
277282 sent_params = call_args [1 ].get ("params" , {})
278283 sent_sql = sent_params .get ("sql" , "" )
279284 assert "TOP 50" in sent_sql
280- assert "TOP 5000" not in sent_sql
0 commit comments