From ad9ad3a814d33de2ce634264e0f2c7be99822176 Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 21 Aug 2026 14:50:20 +0100 Subject: [PATCH 1/2] Show more detailed error when table not found --- .../controllers/endpoints_controller.ex | 5 +++ lib/logflare_web/query_error_helpers.ex | 37 +++++++++++++++++++ .../controllers/endpoints_controller_test.exs | 6 +-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/logflare_web/controllers/endpoints_controller.ex b/lib/logflare_web/controllers/endpoints_controller.ex index 84b1f93543..9211998eea 100644 --- a/lib/logflare_web/controllers/endpoints_controller.ex +++ b/lib/logflare_web/controllers/endpoints_controller.ex @@ -2,6 +2,8 @@ defmodule LogflareWeb.EndpointsController do use LogflareWeb, :controller use OpenApiSpex.ControllerSpecs + import Logflare.Utils.Guards, only: [is_non_empty_binary: 1] + require Logger alias Logflare.Backends.QueryError @@ -97,6 +99,9 @@ defmodule LogflareWeb.EndpointsController do {:error, error = %QueryError{}} -> render(conn, "query.json", error: QueryErrorHelpers.query_error_message(error)) + {:error, message} when is_non_empty_binary(message) -> + render(conn, "query.json", error: QueryErrorHelpers.sandbox_query_error_message(message)) + {:error, _errors} -> render(conn, "query.json", error: QueryErrorHelpers.generic_query_error_message()) end diff --git a/lib/logflare_web/query_error_helpers.ex b/lib/logflare_web/query_error_helpers.ex index 7ed4e7f446..8df7c09f5a 100644 --- a/lib/logflare_web/query_error_helpers.ex +++ b/lib/logflare_web/query_error_helpers.ex @@ -50,6 +50,43 @@ defmodule LogflareWeb.QueryErrorHelpers do @spec generic_query_error_message() :: String.t() def generic_query_error_message, do: @generic_query_error_message + @doc """ + Returns a user-facing message for a sandboxed-query validation error, as + returned by `Logflare.Endpoints.run_query/3` when the query fails validation + before ever reaching a backend. + + Only the "unknown table" case is translated into a plain message; anything + else falls back to the generic message, since Logflare's internal CTE + splicing would otherwise leak into a user-facing error and confuse callers + who never wrote a CTE themselves. + + iex> LogflareWeb.QueryErrorHelpers.sandbox_query_error_message("Table not found in CTE: (function_logs)") + ~s(Table "function_logs" does not exist.) + + iex> LogflareWeb.QueryErrorHelpers.sandbox_query_error_message("Multiple CTEs available (first_cte, second_cte). You must specify which one to query using `f:name`") + "Backend error! Retry your query. Please contact support if this continues." + + iex> LogflareWeb.QueryErrorHelpers.sandbox_query_error_message("sql parser error: Expected: SELECT, VALUES, or a subquery in the query body, found: EOF") + "Backend error! Retry your query. Please contact support if this continues." + """ + @spec sandbox_query_error_message(String.t()) :: String.t() + def sandbox_query_error_message(message) when is_binary(message) do + classified_sandbox_error_message(message) || generic_query_error_message() + end + + defp classified_sandbox_error_message("Table not found in CTE: (" <> rest) do + rest |> String.trim_trailing(")") |> unknown_table_message() + end + + defp classified_sandbox_error_message(_message), do: nil + + defp unknown_table_message(names) do + case String.split(names, ", ") do + [single] -> ~s(Table "#{single}" does not exist.) + multiple -> "Tables #{Enum.map_join(multiple, ", ", &~s("#{&1}"))} do not exist." + end + end + @doc """ Whether a backend %QueryError{} was caused by a query timeout. diff --git a/test/logflare_web/controllers/endpoints_controller_test.exs b/test/logflare_web/controllers/endpoints_controller_test.exs index a74ac56c44..18255a9e90 100644 --- a/test/logflare_web/controllers/endpoints_controller_test.exs +++ b/test/logflare_web/controllers/endpoints_controller_test.exs @@ -257,8 +257,7 @@ defmodule LogflareWeb.EndpointsControllerTest do conn |> json_response(200) - assert response["error"] =~ - LogflareWeb.QueryErrorHelpers.generic_query_error_message() + assert response["error"] == LogflareWeb.QueryErrorHelpers.generic_query_error_message() refute response["result"] end @@ -376,8 +375,7 @@ defmodule LogflareWeb.EndpointsControllerTest do conn |> json_response(200) - assert response["error"] =~ - LogflareWeb.QueryErrorHelpers.generic_query_error_message() + assert response["error"] == LogflareWeb.QueryErrorHelpers.generic_query_error_message() refute response["result"] end From 81c3b2a8fc2e2181e97d98be157afee1c3ddde4b Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 21 Aug 2026 14:56:17 +0100 Subject: [PATCH 2/2] Add test --- .../controllers/endpoints_controller_test.exs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/logflare_web/controllers/endpoints_controller_test.exs b/test/logflare_web/controllers/endpoints_controller_test.exs index 18255a9e90..40dad2b2bc 100644 --- a/test/logflare_web/controllers/endpoints_controller_test.exs +++ b/test/logflare_web/controllers/endpoints_controller_test.exs @@ -501,6 +501,33 @@ defmodule LogflareWeb.EndpointsControllerTest do assert String.downcase(sql) =~ "select 2" end + test "sql referencing a table not in the endpoint's CTE returns a specific error", %{ + conn: conn, + user: user + } do + reject(&BigQueryJobs.bigquery_jobs_query/3) + + backend = insert(:backend, type: :clickhouse, user: user) + + endpoint = + insert(:endpoint, + user: user, + backend: backend, + language: :ch_sql, + enable_auth: true, + sandboxable: true, + query: "with a as (select 1 as b) select b from a" + ) + + conn = + conn + |> put_req_header("x-api-key", user.api_key) + |> get(~p"/api/endpoints/query/#{endpoint.name}", %{sql: "select b from function_logs"}) + + assert json_response(conn, 200)["error"] == ~s(Table "function_logs" does not exist.) + refute json_response(conn, 200)["result"] + end + test "LQL params in GET query string", %{conn: conn, user: user} do pid = self()