66
77from textual .widgets import Tree
88
9- from sqlit .domains .query .store .saved_queries import SavedQueryNameError
9+ from sqlit .domains .query .store .saved_queries import (
10+ SavedQueryConflictError ,
11+ SavedQueryNameError ,
12+ )
1013from sqlit .shared .ui .protocols import TreeMixinHost
1114
1215from ..tree import builder as tree_builder
@@ -96,12 +99,7 @@ def on_tree_node_expanded(self: TreeMixinHost, event: Tree.NodeExpanded) -> None
9699
97100 if self ._get_node_kind (node ) == "connection" :
98101 config = getattr (data , "config" , None )
99- if (
100- config
101- and self .current_config
102- and self .current_config .name == config .name
103- and not list (node .children )
104- ):
102+ if config and self .current_config and self .current_config .name == config .name and not list (node .children ):
105103 tree_builder .populate_connected_tree (self )
106104 return
107105
@@ -242,6 +240,181 @@ def open_query() -> None:
242240 else :
243241 open_query ()
244242
243+ def _saved_query_document_moved (
244+ self : TreeMixinHost ,
245+ connection_name : str ,
246+ old_path : str ,
247+ new_path : str ,
248+ * ,
249+ folder : bool ,
250+ ) -> None :
251+ document = getattr (self , "_query_document" , None )
252+ if document is None or document .connection_name != connection_name :
253+ return
254+ current = document .relative_path
255+ if not current :
256+ return
257+ if folder :
258+ prefix = f"{ old_path } /"
259+ if not current .startswith (prefix ):
260+ return
261+ replacement = f"{ new_path } /{ current [len (prefix ) :]} "
262+ elif current == old_path :
263+ replacement = new_path
264+ else :
265+ return
266+ setter = getattr (self , "_set_query_document" , None )
267+ if callable (setter ):
268+ setter (
269+ connection_name = connection_name ,
270+ relative_path = replacement ,
271+ saved_text = document .saved_text ,
272+ fingerprint = document .fingerprint ,
273+ )
274+
275+ def _saved_query_document_deleted (
276+ self : TreeMixinHost ,
277+ connection_name : str ,
278+ path : str ,
279+ * ,
280+ folder : bool ,
281+ ) -> None :
282+ document = getattr (self , "_query_document" , None )
283+ if document is None or document .connection_name != connection_name :
284+ return
285+ current = document .relative_path
286+ matches = current == path or (folder and bool (current ) and current .startswith (f"{ path } /" ))
287+ if not matches :
288+ return
289+ setter = getattr (self , "_set_query_document" , None )
290+ if callable (setter ):
291+ setter (
292+ connection_name = connection_name ,
293+ relative_path = None ,
294+ saved_text = "" ,
295+ fingerprint = None ,
296+ )
297+
298+ def action_rename_saved_query (self : TreeMixinHost ) -> None :
299+ node = self .object_tree .cursor_node
300+ if not node or not node .data :
301+ return
302+ data = node .data
303+ kind = self ._get_node_kind (node )
304+ if kind == "saved_query_file" :
305+ from sqlit .domains .query .ui .screens import SavedQueryNameScreen
306+
307+ old_path = data .entry .relative_path
308+
309+ def on_name (name : str | None ) -> None :
310+ if not name :
311+ return
312+ try :
313+ entry = self .services .saved_query_store .rename_file (
314+ data .connection_name ,
315+ old_path ,
316+ name ,
317+ expected = data .entry .fingerprint ,
318+ )
319+ except FileExistsError :
320+ self .notify ("A saved query already uses that name" , severity = "warning" )
321+ return
322+ except (OSError , SavedQueryConflictError , SavedQueryNameError ) as exc :
323+ self .notify (f"Could not rename saved query: { exc } " , severity = "error" )
324+ return
325+ self ._saved_query_document_moved (
326+ data .connection_name ,
327+ old_path ,
328+ entry .relative_path ,
329+ folder = False ,
330+ )
331+ self ._refresh_connection_tree ()
332+ self .notify (f"Renamed to { entry .relative_path } " )
333+
334+ self .push_screen (
335+ SavedQueryNameScreen (initial_name = old_path , title = "Rename Saved Query" ),
336+ on_name ,
337+ )
338+ return
339+ if kind != "saved_query_folder" or not data .relative_path :
340+ return
341+ from sqlit .domains .connections .ui .screens import FolderInputScreen
342+
343+ old_path = data .relative_path
344+
345+ def on_folder (name : str | None ) -> None :
346+ if not name :
347+ return
348+ try :
349+ new_path = self .services .saved_query_store .rename_folder (data .connection_name , old_path , name )
350+ except FileExistsError :
351+ self .notify ("A saved-query folder already uses that name" , severity = "warning" )
352+ return
353+ except (OSError , SavedQueryNameError ) as exc :
354+ self .notify (f"Could not rename saved-query folder: { exc } " , severity = "error" )
355+ return
356+ self ._saved_query_document_moved (data .connection_name , old_path , new_path , folder = True )
357+ self ._refresh_connection_tree ()
358+ self .notify (f"Renamed folder to { new_path } " )
359+
360+ self .push_screen (
361+ FolderInputScreen (
362+ data .connection_name ,
363+ current_value = old_path ,
364+ title = "Rename Saved-Query Folder" ,
365+ description = "New folder path (use / for nesting):" ,
366+ ),
367+ on_folder ,
368+ )
369+
370+ def action_delete_saved_query (self : TreeMixinHost ) -> None :
371+ node = self .object_tree .cursor_node
372+ if not node or not node .data :
373+ return
374+ data = node .data
375+ kind = self ._get_node_kind (node )
376+ if kind == "saved_query_file" :
377+ path = data .entry .relative_path
378+ folder = False
379+ description = "The query text will remain open if this file is active."
380+ elif kind == "saved_query_folder" and data .relative_path :
381+ path = data .relative_path
382+ folder = True
383+ count = sum (1 for entry in self .services .saved_query_store .list_for_connection (data .connection_name ) if entry .relative_path .startswith (f"{ path } /" ))
384+ description = f"This permanently deletes { count } saved query file{ 's' if count != 1 else '' } ."
385+ else :
386+ return
387+ from sqlit .shared .ui .screens .confirm import ConfirmScreen
388+
389+ def on_confirm (confirmed : bool | None ) -> None :
390+ if not confirmed :
391+ return
392+ try :
393+ if folder :
394+ self .services .saved_query_store .delete_folder (data .connection_name , path )
395+ else :
396+ self .services .saved_query_store .delete_file (
397+ data .connection_name ,
398+ path ,
399+ expected = data .entry .fingerprint ,
400+ )
401+ except (OSError , SavedQueryConflictError , SavedQueryNameError ) as exc :
402+ self .notify (f"Could not delete saved query: { exc } " , severity = "error" )
403+ return
404+ self ._saved_query_document_deleted (data .connection_name , path , folder = folder )
405+ self ._refresh_connection_tree ()
406+ self .notify (f"Deleted { path } " )
407+
408+ self .push_screen (
409+ ConfirmScreen (
410+ f"Delete { 'folder' if folder else 'saved query' } '{ path } '?" ,
411+ description ,
412+ yes_label = "Delete" ,
413+ no_label = "Cancel" ,
414+ ),
415+ on_confirm ,
416+ )
417+
245418 def on_tree_node_highlighted (self : TreeMixinHost , event : Tree .NodeHighlighted ) -> None :
246419 """Update footer when tree selection changes."""
247420 self ._update_footer_bindings ()
@@ -445,9 +618,7 @@ async def work_async() -> None:
445618 incoming_fks : list [Any ] = []
446619 try :
447620 runtime = getattr (self .services , "runtime" , None )
448- use_worker = bool (getattr (runtime , "process_worker" , False )) and not bool (
449- getattr (getattr (runtime , "mock" , None ), "enabled" , False )
450- )
621+ use_worker = bool (getattr (runtime , "process_worker" , False )) and not bool (getattr (getattr (runtime , "mock" , None ), "enabled" , False ))
451622 client = None
452623 if use_worker and hasattr (self , "_get_process_worker_client_async" ):
453624 client = await self ._get_process_worker_client_async () # type: ignore[attr-defined]
@@ -473,28 +644,37 @@ async def work_async() -> None:
473644 schema_service = self ._get_schema_service ()
474645 if schema_service :
475646 if not columns :
476- columns = await asyncio .to_thread (
477- schema_service .list_columns ,
478- database ,
479- schema ,
480- name ,
481- ) or []
647+ columns = (
648+ await asyncio .to_thread (
649+ schema_service .list_columns ,
650+ database ,
651+ schema ,
652+ name ,
653+ )
654+ or []
655+ )
482656 try :
483- outgoing_fks = await asyncio .to_thread (
484- schema_service .list_foreign_keys ,
485- database ,
486- schema ,
487- name ,
488- ) or []
657+ outgoing_fks = (
658+ await asyncio .to_thread (
659+ schema_service .list_foreign_keys ,
660+ database ,
661+ schema ,
662+ name ,
663+ )
664+ or []
665+ )
489666 except Exception :
490667 outgoing_fks = []
491668 try :
492- incoming_fks = await asyncio .to_thread (
493- schema_service .list_referencing_foreign_keys ,
494- database ,
495- schema ,
496- name ,
497- ) or []
669+ incoming_fks = (
670+ await asyncio .to_thread (
671+ schema_service .list_referencing_foreign_keys ,
672+ database ,
673+ schema ,
674+ name ,
675+ )
676+ or []
677+ )
498678 except Exception :
499679 incoming_fks = []
500680 except Exception :
@@ -530,11 +710,7 @@ def _apply_last_query_table_columns(
530710 table_info = getattr (self , "_last_query_table" , None )
531711 if not table_info :
532712 return
533- if (
534- table_info .get ("database" ) != database
535- or table_info .get ("schema" ) != schema
536- or table_info .get ("name" ) != name
537- ):
713+ if table_info .get ("database" ) != database or table_info .get ("schema" ) != schema or table_info .get ("name" ) != name :
538714 return
539715 table_info ["columns" ] = columns
540716 if foreign_keys is not None :
0 commit comments