From f82412a04e92c439c36b0be969d59d50c6db25f5 Mon Sep 17 00:00:00 2001 From: Karl-Aksel Puulmann Date: Mon, 10 Feb 2025 10:15:39 +0200 Subject: [PATCH] Add `with_cte_expression` ClickHouse supports two different sorts of CTEs: 1. Normal, subquery based CTEs supported by most DBMS 2. Expression-based CTEs This PR adds support for (2) by adding a new adapter-specific method. Note the SQL syntax is flipped for (2). --- lib/ecto/adapters/clickhouse/api.ex | 31 +++++++++++++++++++ lib/ecto/adapters/clickhouse/connection.ex | 8 +++-- .../adapters/clickhouse/connection_test.exs | 11 +++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/ecto/adapters/clickhouse/api.ex b/lib/ecto/adapters/clickhouse/api.ex index 7886b0dc..851013d0 100644 --- a/lib/ecto/adapters/clickhouse/api.ex +++ b/lib/ecto/adapters/clickhouse/api.ex @@ -48,4 +48,35 @@ defmodule Ecto.Adapters.ClickHouse.API do # %{query | sources: {nil, schema}} query end + + @doc """ + Builds a common table expression with an (constant) expression instead of a subquery. + + `with_query` must be a fragment to be evaluated as an expression. + + ## Options + + - as: must be a compile-time literal string that is used in the main query to select + the static value. + + https://clickhouse.com/docs/en/sql-reference/statements/select/with#syntax + """ + defmacro with_cte_expression(query, with_query, opts) do + name = opts[:as] + + if !name do + Ecto.Query.Builder.error!("`as` option must be specified") + end + + # :HACK: We override the operation to :update_all to pass context to the connection. + # :update_all is not used within ClickHouse adapter otherwise. + Ecto.Query.Builder.CTE.build( + query, + name, + with_query, + opts[:materialized], + :update_all, + __CALLER__ + ) + end end diff --git a/lib/ecto/adapters/clickhouse/connection.ex b/lib/ecto/adapters/clickhouse/connection.ex index b513c810..45bce106 100644 --- a/lib/ecto/adapters/clickhouse/connection.ex +++ b/lib/ecto/adapters/clickhouse/connection.ex @@ -369,8 +369,12 @@ defmodule Ecto.Adapters.ClickHouse.Connection do recursive_opt = if recursive, do: "RECURSIVE ", else: "" ctes = - intersperse_map(queries, ?,, fn {name, _opts, cte} -> - [quote_name(name), " AS ", cte_query(cte, sources, params, query)] + intersperse_map(queries, ?,, fn {name, opts, cte} -> + if opts[:operation] == :update_all do + [cte_query(cte, sources, params, query), " AS ", quote_name(name)] + else + [quote_name(name), " AS ", cte_query(cte, sources, params, query)] + end end) ["WITH ", recursive_opt, ctes, " "] diff --git a/test/ecto/adapters/clickhouse/connection_test.exs b/test/ecto/adapters/clickhouse/connection_test.exs index 2efa5673..38806302 100644 --- a/test/ecto/adapters/clickhouse/connection_test.exs +++ b/test/ecto/adapters/clickhouse/connection_test.exs @@ -7,6 +7,8 @@ defmodule Ecto.Adapters.ClickHouse.ConnectionTest do import Ecto.Query import Ecto.Migration, only: [table: 1, table: 2, index: 3, constraint: 3] + require Ecto.Adapters.ClickHouse.API + import Ecto.Adapters.ClickHouse.API, only: [with_cte_expression: 3] defmodule Comment do use Ecto.Schema @@ -233,6 +235,15 @@ defmodule Ecto.Adapters.ClickHouse.ConnectionTest do """ end + test "common table expression with expression instead of subquery" do + query = + Schema + |> with_cte_expression(fragment("123"), as: "value") + |> select([], fragment("value")) + + assert all(query) == "WITH 123 AS \"value\" SELECT value FROM \"schema\" AS s0" + end + test "reference common table in union" do comments_scope_query = "comments"