Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.9.4 (2026-05-30)

- Escape double quotes, backticks, and backslashes in quoted ClickHouse identifiers https://github.com/plausible/ecto_ch/pull/283
- Support the `:comment` column option in migrations (`create`, `add`, and `modify`), which was previously silently ignored https://github.com/plausible/ecto_ch/pull/282

## 0.9.3 (2026-05-07)
Expand Down
15 changes: 14 additions & 1 deletion lib/ecto/adapters/clickhouse/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ defmodule Ecto.Adapters.ClickHouse.Connection do
names
|> Enum.reject(&is_nil/1)
|> intersperse_map(?., &quote_name(&1, nil))
|> escape_quoted(quoter)
|> wrap_in(quoter)
end

Expand All @@ -1032,7 +1033,9 @@ defmodule Ecto.Adapters.ClickHouse.Connection do
end

def quote_name(name, quoter) do
wrap_in(name, quoter)
name
|> escape_quoted(quoter)
|> wrap_in(quoter)
end

defp quote_qualified_name(name, sources, ix) do
Expand All @@ -1052,6 +1055,16 @@ defmodule Ecto.Adapters.ClickHouse.Connection do
defp wrap_in(value, nil), do: value
defp wrap_in(value, wrapper), do: [wrapper, value, wrapper]

defp escape_quoted(value, nil), do: value
defp escape_quoted(value, ?'), do: escape_string(IO.iodata_to_binary(value))

defp escape_quoted(value, quoter) when quoter in [?\", ?`] do
value
|> IO.iodata_to_binary()
|> :binary.replace("\\", "\\\\", [:global])
|> :binary.replace(<<quoter>>, "\\" <> <<quoter>>, [:global])
end

@doc false
# TODO faster?
def escape_string(value) when is_binary(value) do
Expand Down
26 changes: 26 additions & 0 deletions test/ecto/adapters/clickhouse/connection_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,24 @@ defmodule Ecto.Adapters.ClickHouse.ConnectionTest do

query = "schema" |> where(foo: "'") |> select([], true)
assert all(query) == ~s[SELECT true FROM "schema" AS s0 WHERE (s0."foo" = '''')]

query = "schema" |> select([], constant(^"let's \\ escape"))
assert all(query) == ~s[SELECT 'let''s \\\\ escape' FROM "schema" AS s0]
end

test "quoted identifier escape" do
assert Connection.quote_name(~s{has"quote}) |> IO.iodata_to_binary() ==
~s{"has\\"quote"}

assert Connection.quote_name(~s{has\\slash}) |> IO.iodata_to_binary() ==
~s{"has\\\\slash"}

assert Connection.quote_name(~s{has`tick}, ?`) |> IO.iodata_to_binary() ==
~s{`has\\`tick`}

query = insert(nil, ~s{schema"quoted}, [~s{field"quoted}], [], :raise, [])

assert query == ~s{INSERT INTO "schema\\"quoted"("field\\"quoted")}
end

test "binary ops" do
Expand Down Expand Up @@ -2510,6 +2528,14 @@ defmodule Ecto.Adapters.ClickHouse.ConnectionTest do
end
end

test "create table with escaped table comment" do
create = {:create, table(:posts, comment: "owner's \\\\ table"), []}

assert execute_ddl(create) == [
~s{CREATE TABLE "posts" () ENGINE=TinyLog COMMENT 'owner''s \\\\\\\\ table'}
]
end

test "create table with serial primary key" do
create =
{:create, table(:posts, engine: "MergeTree"),
Expand Down
Loading