Skip to content

Commit 8af17b0

Browse files
committed
Add delete-row query action
1 parent ed51434 commit 8af17b0

7 files changed

Lines changed: 105 additions & 0 deletions

File tree

sqlit/core/keymap.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ def _build_action_keys(self) -> list[ActionKeyDef]:
351351
ActionKeyDef("v", "view_cell", "results"),
352352
ActionKeyDef("V", "view_cell_full", "results"),
353353
ActionKeyDef("u", "edit_cell", "results"),
354+
ActionKeyDef("d", "delete_row", "results"),
354355
ActionKeyDef("y", "results_yank_leader_key", "results"),
355356
ActionKeyDef("x", "clear_results", "results"),
356357
ActionKeyDef("slash", "results_filter", "results"),

sqlit/domains/query/ui/mixins/autocomplete.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ def on_text_area_changed(self: AutocompleteMixinHost, event: TextArea.Changed) -
117117
# Mark that text just changed so selection_changed knows to ignore cursor movement
118118
self._text_just_changed = True
119119

120+
if getattr(self, "_suppress_autocomplete_once", False):
121+
self._suppress_autocomplete_once = False
122+
if self._autocomplete_debounce_timer is not None:
123+
self._autocomplete_debounce_timer.stop()
124+
self._autocomplete_debounce_timer = None
125+
self._hide_autocomplete()
126+
return
127+
120128
if self._autocomplete_just_applied:
121129
self._autocomplete_just_applied = False
122130
self._hide_autocomplete()

sqlit/domains/results/state/results_focused.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def has_results(app: InputContext) -> bool:
1818
self.allows("view_cell", has_results, key="v", label="View cell", help="Preview cell (tooltip)")
1919
self.allows("view_cell_full", has_results, key="V", label="View full", help="View full cell value")
2020
self.allows("edit_cell", has_results, key="u", label="Update cell", help="Update cell (generate UPDATE)")
21+
self.allows("delete_row", has_results, key="d", label="Delete row", help="Delete row (generate DELETE)")
2122
self.allows("results_yank_leader_key", has_results, key="y", label="Copy", help="Copy menu (cell/row/all)")
2223
self.allows("clear_results", has_results, key="x", label="Clear", help="Clear results")
2324
self.allows("results_filter", has_results, key="slash", label="Filter", help="Filter rows")
@@ -78,6 +79,13 @@ def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding],
7879
action="edit_cell",
7980
)
8081
)
82+
left.append(
83+
DisplayBinding(
84+
key=resolve_display_key("delete_row") or "d",
85+
label="Delete",
86+
action="delete_row",
87+
)
88+
)
8189
left.append(
8290
DisplayBinding(
8391
key=resolve_display_key("results_yank_leader_key") or "y",
@@ -119,6 +127,7 @@ def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding],
119127
[
120128
"view_cell",
121129
"view_cell_full",
130+
"delete_row",
122131
"results_yank_leader_key",
123132
"clear_results",
124133
"results_filter",

sqlit/domains/results/ui/mixins/results.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,89 @@ def action_clear_results(self: ResultsMixinHost) -> None:
486486
self._last_result_rows = []
487487
self._last_result_row_count = 0
488488

489+
def action_delete_row(self: ResultsMixinHost) -> None:
490+
"""Generate a DELETE query for the selected row and enter insert mode."""
491+
table, columns, _rows, _stacked = self._get_active_results_context()
492+
if not table or table.row_count <= 0:
493+
self.notify("No results", severity="warning")
494+
return
495+
496+
if not columns:
497+
self.notify("No column info", severity="warning")
498+
return
499+
500+
try:
501+
cursor_row, _cursor_col = table.cursor_coordinate
502+
row_values = table.get_row_at(cursor_row)
503+
except Exception:
504+
return
505+
506+
# Format value for SQL
507+
def sql_value(v: object) -> str:
508+
if v is None:
509+
return "NULL"
510+
if isinstance(v, bool):
511+
return "TRUE" if v else "FALSE"
512+
if isinstance(v, int | float):
513+
return str(v)
514+
# String - escape single quotes
515+
return "'" + str(v).replace("'", "''") + "'"
516+
517+
# Get table name and primary key columns
518+
table_name = "<table>"
519+
pk_column_names: set[str] = set()
520+
521+
if hasattr(self, "_last_query_table") and self._last_query_table:
522+
table_info = self._last_query_table
523+
table_name = table_info["name"]
524+
# Get PK columns from column info
525+
for col in table_info.get("columns", []):
526+
if col.is_primary_key:
527+
pk_column_names.add(col.name)
528+
529+
# Build WHERE clause - prefer PK columns, fall back to all columns
530+
where_parts = []
531+
for i, col in enumerate(columns):
532+
if i < len(row_values):
533+
# If we have PK info, only use PK columns; otherwise use all columns
534+
if pk_column_names and col not in pk_column_names:
535+
continue
536+
val = row_values[i]
537+
if val is None:
538+
where_parts.append(f"{col} IS NULL")
539+
else:
540+
where_parts.append(f"{col} = {sql_value(val)}")
541+
542+
# If no where parts (no PKs matched result columns), fall back to all columns
543+
if not where_parts:
544+
for i, col in enumerate(columns):
545+
if i < len(row_values):
546+
val = row_values[i]
547+
if val is None:
548+
where_parts.append(f"{col} IS NULL")
549+
else:
550+
where_parts.append(f"{col} = {sql_value(val)}")
551+
552+
if not where_parts:
553+
self.notify("No row values", severity="warning")
554+
return
555+
556+
where_clause = " AND ".join(where_parts)
557+
558+
# Generate DELETE query for the row
559+
query = f"DELETE FROM {table_name} WHERE {where_clause};"
560+
561+
# Set query and switch to insert mode
562+
self._suppress_autocomplete_once = True
563+
self.query_input.text = query
564+
# Position cursor before the trailing semicolon
565+
cursor_pos = max(len(query) - 1, 0)
566+
self.query_input.cursor_location = (0, cursor_pos)
567+
568+
# Focus query editor but keep NORMAL mode (no INSERT for deletes)
569+
self.action_focus_query()
570+
self._update_footer_bindings()
571+
489572
def action_edit_cell(self: ResultsMixinHost) -> None:
490573
"""Generate an UPDATE query for the selected cell and enter insert mode."""
491574
table, columns, _rows, _stacked = self._get_active_results_context()
@@ -571,6 +654,7 @@ def sql_value(v: object) -> str:
571654
cursor_pos = query.find(set_prefix) + len(set_prefix)
572655

573656
# Set query and switch to insert mode
657+
self._suppress_autocomplete_once = True
574658
self.query_input.text = query
575659
self.query_input.focus()
576660

sqlit/domains/shell/app/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ def __init__(
133133
self._autocomplete_index: int = 0
134134
self._autocomplete_filter: str = ""
135135
self._autocomplete_just_applied: bool = False
136+
self._suppress_autocomplete_once: bool = False
136137
self._value_view_active: bool = False
137138
self._last_result_columns: list[str] = []
138139
self._last_result_rows: list[tuple[Any, ...]] = []

sqlit/domains/shell/state/machine.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def binding(key: str, desc: str, indent: int = 4) -> str:
224224
lines.append(binding("v", "Preview cell (inline)"))
225225
lines.append(binding("V", "View full cell value"))
226226
lines.append(binding("u", "Generate UPDATE statement"))
227+
lines.append(binding("d", "Generate DELETE statement"))
227228
lines.append(binding("/", "Filter rows"))
228229
lines.append(binding("x", "Clear results"))
229230
lines.append(binding("<tab>", "Next result set"))

sqlit/shared/ui/protocols/autocomplete.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class AutocompleteStateProtocol(Protocol):
3333
_autocomplete_just_applied: bool
3434
_autocomplete_visible: bool
3535
_suppress_autocomplete_on_newline: bool
36+
_suppress_autocomplete_once: bool
3637
_autocomplete_debounce_timer: Timer | None
3738
_text_just_changed: bool
3839

0 commit comments

Comments
 (0)