11from __future__ import annotations
22
3+ import builtins
34from dataclasses import dataclass
45from types import SimpleNamespace
56from typing import Any , cast
@@ -14,6 +15,7 @@ class DummyCliArgs:
1415 execute : str | None
1516 format : str = 'tsv'
1617 batch : str | None = None
18+ warn_batch : bool = False
1719 checkpoint : str | None = None
1820
1921
@@ -22,11 +24,21 @@ class DummyFormatter:
2224 format_name : str | None = None
2325
2426
27+ class DummyLogger :
28+ def __init__ (self ) -> None :
29+ self .warning_calls : list [str ] = []
30+
31+ def warning (self , message : str ) -> None :
32+ self .warning_calls .append (message )
33+
34+
2535class DummyMyCli :
2636 def __init__ (self , run_query_error : Exception | None = None ) -> None :
2737 self .main_formatter = DummyFormatter ()
2838 self .run_query_error = run_query_error
2939 self .ran_queries : list [tuple [str , str | None ]] = []
40+ self .destructive_keywords = ['drop' ]
41+ self .logger = DummyLogger ()
3042
3143 def run_query (self , query : str , checkpoint : str | None = None ) -> None :
3244 if self .run_query_error is not None :
@@ -125,3 +137,61 @@ def test_main_execute_from_cli_reports_query_errors(monkeypatch) -> None:
125137 assert mycli .main_formatter .format_name == 'ascii'
126138 assert mycli .ran_queries == []
127139 assert secho_calls == [('boom' , True , 'red' )]
140+
141+
142+ def test_main_execute_from_cli_confirms_destructive_query (monkeypatch ) -> None :
143+ mycli = DummyMyCli ()
144+ tty = object ()
145+ confirm_calls : list [tuple [list [str ], str ]] = []
146+
147+ monkeypatch .setattr (execute_mode , 'sys' , fake_sys (stdin_tty = True ))
148+ monkeypatch .setattr (execute_mode , 'is_destructive' , lambda keywords , query : True )
149+ monkeypatch .setattr (builtins , 'open' , lambda path : tty )
150+
151+ def confirm_destructive_query (keywords : list [str ], query : str ) -> bool :
152+ confirm_calls .append ((keywords , query ))
153+ return True
154+
155+ monkeypatch .setattr (execute_mode , 'confirm_destructive_query' , confirm_destructive_query )
156+
157+ result = main_execute_from_cli (mycli , DummyCliArgs (execute = 'drop table t' , warn_batch = True ))
158+
159+ assert result == 0
160+ assert execute_mode .sys .stdin is tty
161+ assert confirm_calls == [(['drop' ], 'drop table t' )]
162+ assert mycli .ran_queries == [('drop table t' , None )]
163+
164+
165+ def test_main_execute_from_cli_returns_error_when_destructive_query_is_rejected (monkeypatch ) -> None :
166+ mycli = DummyMyCli ()
167+
168+ monkeypatch .setattr (execute_mode , 'sys' , fake_sys (stdin_tty = True ))
169+ monkeypatch .setattr (execute_mode , 'is_destructive' , lambda keywords , query : True )
170+ monkeypatch .setattr (builtins , 'open' , lambda path : object ())
171+ monkeypatch .setattr (execute_mode , 'confirm_destructive_query' , lambda keywords , query : False )
172+
173+ result = main_execute_from_cli (mycli , DummyCliArgs (execute = 'drop table t' , warn_batch = True ))
174+
175+ assert result == 1
176+ assert mycli .ran_queries == []
177+
178+
179+ def test_main_execute_from_cli_reports_tty_open_error_for_destructive_query (monkeypatch ) -> None :
180+ secho_calls : list [tuple [str , bool , str ]] = []
181+ mycli = DummyMyCli ()
182+
183+ monkeypatch .setattr (execute_mode , 'sys' , fake_sys (stdin_tty = True ))
184+ monkeypatch .setattr (execute_mode , 'is_destructive' , lambda keywords , query : True )
185+ monkeypatch .setattr (builtins , 'open' , lambda path : (_ for _ in ()).throw (OSError ('no tty' )))
186+ monkeypatch .setattr (
187+ execute_mode .click ,
188+ 'secho' ,
189+ lambda message , err , fg : secho_calls .append ((message , err , fg )),
190+ )
191+
192+ result = main_execute_from_cli (mycli , DummyCliArgs (execute = 'drop table t' , warn_batch = True ))
193+
194+ assert result == 1
195+ assert mycli .logger .warning_calls == ['Unable to open TTY as stdin.' ]
196+ assert mycli .ran_queries == []
197+ assert secho_calls == [('no tty' , True , 'red' )]
0 commit comments