defmodule SelectoComponents.Components.SqlDebug do
@moduledoc """
Component for displaying SQL queries in development mode.
Shows prettified SQL with syntax highlighting and copy functionality.
"""
use Phoenix.Component
alias Phoenix.LiveView.JS
@doc """
Renders SQL debug information if in dev mode.
## Attributes
- sql: The SQL query string
- params: Query parameters (optional)
- expanded: Whether the debug section is initially expanded
"""
attr(:sql, :string, default: nil)
attr(:params, :list, default: [])
attr(:expanded, :boolean, default: false)
attr(:execution_time, :integer, default: nil)
def sql_debug(assigns) do
# Only show in development mode
if Mix.env() == :dev && assigns.sql do
assigns =
assigns
|> Map.put(:formatted_sql, format_sql(assigns.sql))
|> Map.put(:debug_id, "sql_debug_#{:erlang.unique_integer([:positive])}")
~H"""
SQL Debug
<%= if @execution_time do %>
(<%= @execution_time %>ms)
<% end %>
"_sql")}
>
Copy SQL
Query
"_sql"} class="sql-code bg-gray-900 dark:bg-gray-800 text-gray-100 dark:text-gray-200 p-3 rounded overflow-x-auto text-xs"><%= @formatted_sql %>
<%= if @params && length(@params) > 0 do %>
Parameters
<%= for {param, idx} <- Enum.with_index(@params, 1) do %>
$<%= idx %>:
<%= inspect(param) %>
<% end %>
<% end %>
"""
else
~H""
end
end
@doc """
Renders a simplified inline SQL display
"""
attr(:sql, :string, required: true)
def sql_inline(assigns) do
if Mix.env() == :dev do
~H"""
<%= truncate_sql(@sql) %>
"""
else
~H""
end
end
# Private functions
defp format_sql(sql) do
sql
|> add_syntax_highlighting()
|> indent_sql()
end
defp add_syntax_highlighting(sql) do
# Basic SQL syntax highlighting
keywords = ~w(
select from where join left right inner outer on
group by order having limit offset distinct
as and or not in exists between like ilike
case when then else end null is true false
insert into values update set delete
create table alter drop index primary key foreign references
begin commit rollback transaction
with recursive union all intersect except
json_agg array_agg row_to_json to_json jsonb_agg
)
# Replace keywords with highlighted versions
Enum.reduce(keywords, sql, fn keyword, acc ->
# Case-insensitive replacement
Regex.replace(
~r/\b#{keyword}\b/i,
acc,
"#{String.upcase(keyword)} "
)
end)
end
defp indent_sql(sql) do
# Basic SQL indentation for readability
sql
|> String.replace("SELECT", "\nSELECT")
|> String.replace("FROM", "\nFROM")
|> String.replace("WHERE", "\nWHERE")
|> String.replace("JOIN", "\n JOIN")
|> String.replace("LEFT JOIN", "\n LEFT JOIN")
|> String.replace("RIGHT JOIN", "\n RIGHT JOIN")
|> String.replace("GROUP BY", "\nGROUP BY")
|> String.replace("ORDER BY", "\nORDER BY")
|> String.replace("HAVING", "\nHAVING")
|> String.replace("LIMIT", "\nLIMIT")
|> String.trim()
end
defp truncate_sql(sql, max_length \\ 100) do
if String.length(sql) > max_length do
String.slice(sql, 0, max_length) <> "..."
else
sql
end
end
defp toggle_debug(debug_id) do
JS.toggle(to: "##{debug_id}")
|> JS.toggle_class("rotate-90", to: "[data-debug='#{debug_id}']")
end
defp copy_to_clipboard(element_id) do
JS.dispatch("phx:copy", to: "##{element_id}")
end
@doc """
JavaScript hooks for SQL debug functionality
"""
def hooks do
%{
"SqlDebug" => %{
mounted: """
// Copy to clipboard functionality
this.el.addEventListener('phx:copy', e => {
const text = e.target.textContent;
navigator.clipboard.writeText(text).then(() => {
// Show success feedback
const button = e.target.closest('.sql-debug-container').querySelector('[phx-click*="copy"]');
const originalText = button.textContent;
button.textContent = 'Copied!';
button.classList.add('bg-green-50', 'text-green-700');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('bg-green-50', 'text-green-700');
}, 2000);
});
});
"""
}
}
end
@doc """
CSS styles for SQL debug display
"""
def styles do
"""
.sql-debug-container {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.sql-code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
line-height: 1.5;
}
.sql-keyword {
color: #ff79c6;
font-weight: bold;
}
.sql-function {
color: #50fa7b;
}
.sql-string {
color: #f1fa8c;
}
.sql-number {
color: #bd93f9;
}
.sql-comment {
color: #6272a4;
font-style: italic;
}
.sql-inline {
max-width: 300px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
"""
end
end