Skip to content
Open
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
20 changes: 20 additions & 0 deletions sqlit/domains/connections/providers/adapters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,26 @@ def get_sequence_definition(
"cycle": None,
}

def get_procedure_definition(
self, conn: Any, procedure_name: str, database: str | None = None
) -> dict[str, Any]:
"""Get detailed information about a stored procedure.

Returns a dict with keys like:
- name: Procedure name
- schema: Schema/database the procedure belongs to
- language: Implementation language (e.g. SQL)
- definition: Procedure source code/DDL

Default implementation returns minimal info. Override in subclasses.
"""
return {
"name": procedure_name,
"schema": database or "",
"language": None,
"definition": None,
}

@abstractmethod
def quote_identifier(self, name: str) -> str:
"""Quote an identifier (table name, column name, etc.)."""
Expand Down
4 changes: 4 additions & 0 deletions sqlit/domains/connections/providers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ def get_sequence_definition(self, conn: Any, sequence_name: str, database: str |
class ProcedureInspector(Protocol):
def get_procedures(self, conn: Any, database: str | None = None) -> list[Any]: ...

def get_procedure_definition(
self, conn: Any, procedure_name: str, database: str | None = None
) -> dict[str, Any]: ...


class ConfigValidator(Protocol):
def normalize(self, config: ConnectionConfig) -> ConnectionConfig: ...
Expand Down
26 changes: 26 additions & 0 deletions sqlit/domains/connections/providers/mysql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,29 @@ def get_trigger_definition(
"event": None,
"definition": None,
}

def get_procedure_definition(
self, conn: Any, procedure_name: str, database: str | None = None
) -> dict[str, Any]:
"""Get detailed information about a MySQL/MariaDB stored procedure."""
cursor = conn.cursor()
if database:
cursor.execute(
f"SHOW CREATE PROCEDURE {self.quote_identifier(database)}.{self.quote_identifier(procedure_name)}"
)
else:
cursor.execute(f"SHOW CREATE PROCEDURE {self.quote_identifier(procedure_name)}")
row = cursor.fetchone()
if row:
return {
"name": procedure_name,
"schema": database or "",
"language": "SQL",
"definition": row[2] if len(row) > 2 else None,
}
return {
"name": procedure_name,
"schema": database or "",
"language": None,
"definition": None,
}
10 changes: 10 additions & 0 deletions sqlit/domains/explorer/app/schema_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,13 @@ def get_sequence_definition(self, database: str | None, name: str) -> dict[str,
lambda: inspector.get_sequence_definition(self.session.connection, name, db_arg),
database,
)

def get_procedure_definition(self, database: str | None, name: str) -> dict[str, Any] | None:
inspector = self.session.provider.schema_inspector
if not isinstance(inspector, ProcedureInspector):
return None
db_arg = self._resolve_db_arg(database)
return self._run_with_retry(
lambda: inspector.get_procedure_definition(self.session.connection, name, db_arg),
database,
)
4 changes: 2 additions & 2 deletions sqlit/domains/explorer/state/tree_on_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class TreeOnObjectState(State):
"""Tree focused on index, trigger, or sequence node."""
"""Tree focused on index, trigger, sequence, or procedure node."""

help_category = "Explorer"

Expand Down Expand Up @@ -45,4 +45,4 @@ def get_display_bindings(self, app: InputContext) -> tuple[list[DisplayBinding],
return left, []

def is_active(self, app: InputContext) -> bool:
return app.focus == "explorer" and app.tree_node_kind in ("index", "trigger", "sequence")
return app.focus == "explorer" and app.tree_node_kind in ("index", "trigger", "sequence", "procedure")
4 changes: 4 additions & 0 deletions sqlit/domains/explorer/ui/mixins/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ def action_select_table(self: TreeMixinHost) -> None:
tree_object_info.show_sequence_info(self, data)
return

if self._get_node_kind(node) == "procedure":
tree_object_info.show_procedure_info(self, data)
return

def action_use_database(self: TreeMixinHost) -> None:
"""Toggle the selected database as the default for the current connection."""
node = self.object_tree.cursor_node
Expand Down
51 changes: 48 additions & 3 deletions sqlit/domains/explorer/ui/tree/object_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Any

from sqlit.domains.explorer.domain.tree_nodes import IndexNode, SequenceNode, TriggerNode
from sqlit.domains.explorer.domain.tree_nodes import IndexNode, ProcedureNode, SequenceNode, TriggerNode
from sqlit.shared.ui.protocols import TreeMixinHost


Expand Down Expand Up @@ -56,7 +56,49 @@ def show_sequence_info(host: TreeMixinHost, data: SequenceNode) -> None:
host.notify(f"Error getting sequence info: {error}", severity="error")


def display_object_info(host: TreeMixinHost, object_type: str, info: dict[str, Any]) -> None:
def show_procedure_info(host: TreeMixinHost, data: ProcedureNode) -> None:
"""Show stored procedure definition in the results panel and load it into the query editor."""
schema_service = host._get_schema_service()
if not schema_service:
return

try:
info = schema_service.get_procedure_definition(data.database, data.name)
if info is None:
host.notify("Stored procedures not supported for this database.", severity="warning")
return
_prepare_procedure_for_edit(host, data, info)
display_object_info(host, "Procedure", info, editable_definition=True)
except Exception as error:
host.notify(f"Error getting procedure info: {error}", severity="error")


def _prepare_procedure_for_edit(host: TreeMixinHost, data: ProcedureNode, info: dict[str, Any]) -> None:
"""Prepend DROP statement and wrap MySQL/MariaDB DDL in DELIMITER blocks."""
definition = info.get("definition")
if not definition:
return

provider = getattr(host, "current_provider", None)
db_type = provider.metadata.db_type if provider else ""
if db_type in ("mysql", "mariadb"):
quoted_name = f"`{data.name.replace('`', '``')}`"
info["definition"] = (
f"DROP PROCEDURE IF EXISTS {quoted_name};\n"
"DELIMITER $$\n"
f"{definition}\n"
"$$\n"
"DELIMITER ;"
)


def display_object_info(
host: TreeMixinHost,
object_type: str,
info: dict[str, Any],
*,
editable_definition: bool = False,
) -> None:
"""Display object info in the results table as a Property/Value view."""
rows: list[tuple[str, str]] = []
for key, value in info.items():
Expand All @@ -81,4 +123,7 @@ def display_object_info(host: TreeMixinHost, object_type: str, info: dict[str, A

definition = info.get("definition")
if definition:
host.query_input.text = f"/*\n{definition}\n*/"
if editable_definition:
host.query_input.text = str(definition)
else:
host.query_input.text = f"/*\n{definition}\n*/"
Loading