Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sqlit/core/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,8 @@ def _build_leader_commands(self) -> list[LeaderCommandDef]:
LeaderCommandDef("c", "cell", "Copy cell", "Copy", menu="ry"),
LeaderCommandDef("y", "row", "Copy row", "Copy", menu="ry"),
LeaderCommandDef("a", "all", "Copy all", "Copy", menu="ry"),
LeaderCommandDef("i", "insert", "Copy INSERT", "Copy", menu="ry"),
LeaderCommandDef("i", "insert", "Copy INSERT (row)", "Copy", menu="ry"),
LeaderCommandDef("I", "insert_all", "Copy INSERT (all)", "Copy", menu="ry"),
LeaderCommandDef("e", "export", "Export...", "Export", menu="ry"),
# rye results export menu
LeaderCommandDef("c", "csv", "Export as CSV", "Export", menu="rye"),
Expand Down
57 changes: 51 additions & 6 deletions sqlit/domains/results/ui/mixins/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,17 @@ def action_ry_all(self: ResultsMixinHost) -> None:
if table:
self._flash_table_yank(table, "all")

def _build_insert_values_clause(self: ResultsMixinHost, columns: list[str], row_values: tuple[Any, ...]) -> str:
insert_columns = list(columns[: len(row_values)])
value_list = ", ".join(self._format_sql_value(value) for value in row_values[: len(insert_columns)])
return f"({value_list})"

def _build_insert_statement(self: ResultsMixinHost, table_name: str, columns: list[str], row_values: tuple[Any, ...]) -> str:
insert_columns = list(columns[: len(row_values)])
column_list = ", ".join(insert_columns)
values_clause = self._build_insert_values_clause(columns, row_values)
return f"INSERT INTO {table_name} ({column_list}) VALUES {values_clause};"

def action_ry_insert(self: ResultsMixinHost) -> None:
"""Copy an INSERT statement for the selected row (from yank menu)."""
self._clear_leader_pending()
Expand All @@ -655,8 +666,35 @@ def action_ry_insert(self: ResultsMixinHost) -> None:
if row_values is None:
return

insert_columns = list(columns[: len(row_values)])
if not insert_columns:
table_info = self._get_active_results_table_info(table, _stacked)
table_name = "<table>"
if table_info:
table_name = table_info.get("name") or table_name

query = self._build_insert_statement(table_name, columns, row_values)

self._copy_text(query)
self._flash_table_yank(table, "row")

def action_ry_insert_all(self: ResultsMixinHost) -> None:
"""Copy INSERT statements for all currently visible rows (from yank menu)."""
self._clear_leader_pending()
table, columns, rows, _stacked = self._get_active_results_context()
if not table or table.row_count <= 0:
self.notify("No results", severity="warning")
return

if not columns:
self.notify("No column info", severity="warning")
return

matching_rows = getattr(self, "_results_filter_matching_rows", None)
source_rows = (
list(matching_rows)
if matching_rows is not None and len(matching_rows) == table.row_count
else list(rows[: table.row_count])
)
if not source_rows:
self.notify("No row values", severity="warning")
return

Expand All @@ -665,12 +703,19 @@ def action_ry_insert(self: ResultsMixinHost) -> None:
if table_info:
table_name = table_info.get("name") or table_name

column_list = ", ".join(insert_columns)
value_list = ", ".join(self._format_sql_value(value) for value in row_values[: len(insert_columns)])
query = f"INSERT INTO {table_name} ({column_list}) VALUES ({value_list});"
if len(source_rows) == 1:
query = self._build_insert_statement(table_name, columns, tuple(source_rows[0]))
else:
first_row = tuple(source_rows[0])
insert_columns = list(columns[: len(first_row)])
column_list = ", ".join(insert_columns)
values_clauses = ",\n".join(
self._build_insert_values_clause(columns, tuple(row_values)) for row_values in source_rows
)
query = f"INSERT INTO {table_name} ({column_list}) VALUES\n{values_clauses};"

self._copy_text(query)
self._flash_table_yank(table, "row")
self._flash_table_yank(table, "all")

def action_ry_export(self: ResultsMixinHost) -> None:
"""Open the export submenu."""
Expand Down
3 changes: 2 additions & 1 deletion sqlit/domains/shell/state/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ def binding(key: str, desc: str, indent: int = 4) -> str:
lines.append(binding("yc", "Copy cell"))
lines.append(binding("yy", "Copy row"))
lines.append(binding("ya", "Copy all"))
lines.append(binding("yi", "Copy INSERT"))
lines.append(binding("yi", "Copy INSERT (row)"))
lines.append(binding("yI", "Copy INSERT (all visible rows)"))
lines.append(binding("ye", "Export menu..."))
lines.append("")

Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_results_yank_insert.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,31 @@ def test_action_ry_row_uses_unrendered_values_during_search_highlight() -> None:
host.action_ry_row()

assert host.copied_text == "aaa_bbb"


def test_ry_leader_menu_resolves_shift_i_to_ry_insert_all() -> None:
keymap = get_keymap()
assert keymap.leader("insert_all", menu="ry") == "I"


def test_action_ry_insert_all_copies_all_rows_as_single_insert_statement() -> None:
host = _Host(["id", "name"], [(1, "Alice"), (2, "Bob")])

host.action_ry_insert_all()

assert host.copied_text == (
"INSERT INTO users (id, name) VALUES\n"
"(1, 'Alice'),\n"
"(2, 'Bob');"
)
assert host.flashes == [(host.results_table, "all")]


def test_action_ry_insert_all_uses_filtered_rows_only() -> None:
host = _Host(["id", "name"], [(1, "Alice"), (2, "Bob"), (3, "Carol")], filter_visible=True)
host._results_filter_matching_rows = [(2, "Bob")]
host.results_table = _Table([("2", "Bob")])

host.action_ry_insert_all()

assert host.copied_text == "INSERT INTO users (id, name) VALUES (2, 'Bob');"