@@ -179,6 +179,171 @@ fn main() {
179179 ) ;
180180 println ! ( "✓ lint: syntax error → error diagnostic" ) ;
181181
182+ // ── test2.sql: DELETE with WHERE → no corruption ─────────────────────────
183+ // Send lowercase DELETE to guarantee the formatter has something to change.
184+ // Before the fix, "delete from..." was rewritten to "SELECT * FROM WHERE..."
185+ // (no table, SELECT instead of DELETE). After the fix it comes back as
186+ // "DELETE FROM steps WHERE id = $1::uuid".
187+ let sql6 = "delete from steps where id = $1::uuid" ;
188+ send (
189+ & mut stdin,
190+ & format ! (
191+ r#"{{"jsonrpc":"2.0","method":"textDocument/didChange","params":{{"textDocument":{{"uri":"{uri}","version":6}},"contentChanges":[{{"text":{text}}}]}}}}"# ,
192+ uri = uri,
193+ text = serde_json:: to_string( sql6) . unwrap( )
194+ ) ,
195+ ) ;
196+ send (
197+ & mut stdin,
198+ & format ! (
199+ r#"{{"jsonrpc":"2.0","id":6,"method":"textDocument/formatting","params":{{"textDocument":{{"uri":"{uri}"}},"options":{{"tabSize":4,"insertSpaces":true}}}}}}"# ,
200+ uri = uri
201+ ) ,
202+ ) ;
203+ let resp = recv ( & mut reader) ;
204+ // null result = no change (raw text preserved as-is, which is still correct)
205+ // non-null = formatter applied — must contain DELETE and not SELECT
206+ let no_change = resp. contains ( r#""result":null"# ) ;
207+ let correct_format = resp. to_uppercase ( ) . contains ( "DELETE" )
208+ && !resp. to_lowercase ( ) . contains ( r#""newtext":"select"# ) ;
209+ assert ! (
210+ no_change || correct_format,
211+ "DELETE was corrupted by formatter:\n {}" ,
212+ resp
213+ ) ;
214+ println ! ( "✓ format: DELETE preserved (not corrupted to SELECT)" ) ;
215+
216+ // ── test2.sql: INSERT … RETURNING * → RETURNING preserved ────────────────
217+ let sql7 = r#"INSERT INTO steps (id, question) VALUES ($1::uuid, $2) RETURNING *"# ;
218+ send (
219+ & mut stdin,
220+ & format ! (
221+ r#"{{"jsonrpc":"2.0","method":"textDocument/didChange","params":{{"textDocument":{{"uri":"{uri}","version":7}},"contentChanges":[{{"text":{text}}}]}}}}"# ,
222+ uri = uri,
223+ text = serde_json:: to_string( sql7) . unwrap( )
224+ ) ,
225+ ) ;
226+ send (
227+ & mut stdin,
228+ & format ! (
229+ r#"{{"jsonrpc":"2.0","id":7,"method":"textDocument/formatting","params":{{"textDocument":{{"uri":"{uri}"}},"options":{{"tabSize":4,"insertSpaces":true}}}}}}"# ,
230+ uri = uri
231+ ) ,
232+ ) ;
233+ let resp = recv ( & mut reader) ;
234+ assert ! (
235+ resp. to_uppercase( ) . contains( "RETURNING" ) ,
236+ "RETURNING dropped from INSERT:\n {}" ,
237+ resp
238+ ) ;
239+ println ! ( "✓ format: INSERT … RETURNING * preserved" ) ;
240+
241+ // ── test2.sql: UPDATE … RETURNING * → RETURNING preserved ────────────────
242+ let sql8 = "UPDATE steps SET question = $2 WHERE id = $1::uuid RETURNING *" ;
243+ send (
244+ & mut stdin,
245+ & format ! (
246+ r#"{{"jsonrpc":"2.0","method":"textDocument/didChange","params":{{"textDocument":{{"uri":"{uri}","version":8}},"contentChanges":[{{"text":{text}}}]}}}}"# ,
247+ uri = uri,
248+ text = serde_json:: to_string( sql8) . unwrap( )
249+ ) ,
250+ ) ;
251+ send (
252+ & mut stdin,
253+ & format ! (
254+ r#"{{"jsonrpc":"2.0","id":8,"method":"textDocument/formatting","params":{{"textDocument":{{"uri":"{uri}"}},"options":{{"tabSize":4,"insertSpaces":true}}}}}}"# ,
255+ uri = uri
256+ ) ,
257+ ) ;
258+ let resp = recv ( & mut reader) ;
259+ assert ! (
260+ resp. to_uppercase( ) . contains( "RETURNING" ) ,
261+ "RETURNING dropped from UPDATE:\n {}" ,
262+ resp
263+ ) ;
264+ println ! ( "✓ format: UPDATE … RETURNING * preserved" ) ;
265+
266+ // ── test2.sql: LIMIT $n OFFSET $n → no syntax error diagnostic ───────────
267+ let sql9 = "SELECT * FROM steps ORDER BY order_index LIMIT $1 OFFSET $2" ;
268+ send (
269+ & mut stdin,
270+ & format ! (
271+ r#"{{"jsonrpc":"2.0","method":"textDocument/didChange","params":{{"textDocument":{{"uri":"{uri}","version":9}},"contentChanges":[{{"text":{text}}}]}}}}"# ,
272+ uri = uri,
273+ text = serde_json:: to_string( sql9) . unwrap( )
274+ ) ,
275+ ) ;
276+ let notif = recv ( & mut reader) ;
277+ assert ! (
278+ notif. contains( "publishDiagnostics" ) ,
279+ "expected publishDiagnostics:\n {}" ,
280+ notif
281+ ) ;
282+ // There should be no error-severity diagnostic — only a SELECT * warning
283+ assert ! (
284+ !notif. contains( r#""severity":1"# ) ,
285+ "false positive ERROR for LIMIT $n OFFSET $n:\n {}" ,
286+ notif
287+ ) ;
288+ println ! ( "✓ lint: LIMIT $n OFFSET $n → no false positive syntax error" ) ;
289+
290+ // ── test2.sql: LIMIT $n OFFSET $n → LIMIT preserved in formatted output ──
291+ send (
292+ & mut stdin,
293+ & format ! (
294+ r#"{{"jsonrpc":"2.0","id":9,"method":"textDocument/formatting","params":{{"textDocument":{{"uri":"{uri}"}},"options":{{"tabSize":4,"insertSpaces":true}}}}}}"# ,
295+ uri = uri
296+ ) ,
297+ ) ;
298+ let resp = recv ( & mut reader) ;
299+ assert ! (
300+ resp. to_uppercase( ) . contains( "LIMIT" ) ,
301+ "LIMIT dropped by formatter:\n {}" ,
302+ resp
303+ ) ;
304+ assert ! (
305+ resp. to_uppercase( ) . contains( "OFFSET" ) ,
306+ "OFFSET dropped by formatter:\n {}" ,
307+ resp
308+ ) ;
309+ println ! ( "✓ format: LIMIT $n OFFSET $n preserved" ) ;
310+
311+ // ── test2.sql: SELECT EXISTS(…) → no dangling FROM ───────────────────────
312+ let sql10 = "SELECT EXISTS( SELECT 1 FROM steps WHERE scenario_id = $1 AND order_index = $2 )" ;
313+ send (
314+ & mut stdin,
315+ & format ! (
316+ r#"{{"jsonrpc":"2.0","method":"textDocument/didChange","params":{{"textDocument":{{"uri":"{uri}","version":10}},"contentChanges":[{{"text":{text}}}]}}}}"# ,
317+ uri = uri,
318+ text = serde_json:: to_string( sql10) . unwrap( )
319+ ) ,
320+ ) ;
321+ send (
322+ & mut stdin,
323+ & format ! (
324+ r#"{{"jsonrpc":"2.0","id":10,"method":"textDocument/formatting","params":{{"textDocument":{{"uri":"{uri}"}},"options":{{"tabSize":4,"insertSpaces":true}}}}}}"# ,
325+ uri = uri
326+ ) ,
327+ ) ;
328+ let resp = recv ( & mut reader) ;
329+ assert ! (
330+ resp. to_uppercase( ) . contains( "EXISTS" ) ,
331+ "EXISTS lost from SELECT EXISTS:\n {}" ,
332+ resp
333+ ) ;
334+ // The formatted text must not end with a bare "FROM" (empty-table corruption)
335+ let formatted_text = resp
336+ . split ( r#""newText":""# )
337+ . nth ( 1 )
338+ . unwrap_or ( "" )
339+ . trim_end_matches ( r#"""# ) ;
340+ assert ! (
341+ !formatted_text. trim_end( ) . ends_with( "FROM" ) && !formatted_text. contains( "\\ nFROM \\ n" ) ,
342+ "dangling FROM in SELECT EXISTS output:\n {}" ,
343+ resp
344+ ) ;
345+ println ! ( "✓ format: SELECT EXISTS(…) no dangling FROM" ) ;
346+
182347 // ── shutdown ──────────────────────────────────────────────────────────────
183348 // lsp-server's handle_shutdown may block waiting for "exit" before
184349 // returning the shutdown response, so we send both together then
0 commit comments