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"""

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