Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/moody-maps-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@core/sync-service': patch
---

Add Postgres concat(variadic text) to shape subset WHERE evaluation, with NULL arguments skipped and SqlGenerator support for concat(...) round-trips.
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ defmodule Electric.Replication.Eval.Env.KnownFunctions do
end
end

def concat_text(parts) when is_list(parts) do
parts
|> Enum.reject(&is_nil/1)
|> Enum.join()
end

defpostgres("concat(VARIADIC text) -> text",
strict?: false,
delegate: &__MODULE__.concat_text/1
)

defpostgres("lower(text) -> text", delegate: &String.downcase/1)
defpostgres("upper(text) -> text", delegate: &String.upcase/1)
defpostgres("text ~~ text -> bool", delegate: &Casting.like?/2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ defmodule Electric.Replication.Eval.SqlGenerator do
membership (IN), logical operators (AND, OR, NOT), boolean tests
(IS TRUE, IS FALSE, IS UNKNOWN, etc.), column references, constants
(strings, integers, floats, booleans, NULL), type casts, arithmetic
operators (+, -, *, /, ^, |/, @, &, |, #, ~), string concatenation (||),
operators (+, -, *, /, ^, |/, @, &, |, #, ~), string concatenation (||, concat),
array operators (@>, <@, &&), array/slice access, DISTINCT/NOT DISTINCT,
ANY/ALL, and sublink membership checks.

Expand Down Expand Up @@ -217,6 +217,10 @@ defmodule Electric.Replication.Eval.SqlGenerator do
defp to_sql_prec(%Func{name: "\"&&\"", args: [left, right]}),
do: binary_op(left, "&&", right, @prec_other_op)

# Variadic concat — parser stores arguments as a single Array (see Parser.from_concrete/2).
defp to_sql_prec(%Func{name: "concat", args: [%Array{elements: elements}]}),
do: {"concat(#{Enum.map_join(elements, ", ", &to_sql/1)})", @prec_atom}

# Named functions (lower, upper, like, ilike, array_*, justify_*, timezone, casts, etc.)
# These are Func nodes where the name is a plain identifier (no quotes around operators)
defp to_sql_prec(%Func{name: name, args: args})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,45 @@ defmodule Electric.Replication.Eval.RunnerTest do
|> Runner.execute(%{})
end

test "supports concat as a variadic text function, skipping NULL arguments" do
assert {:ok, "ab"} =
~S|concat('a', 'b')|
|> Parser.parse_and_validate_expression!()
|> Runner.execute(%{})

assert {:ok, "ab"} =
~S|concat('a', NULL::text, 'b')|
|> Parser.parse_and_validate_expression!()
|> Runner.execute(%{})

assert {:ok, ""} =
~S|concat(NULL::text, NULL::text)|
|> Parser.parse_and_validate_expression!()
|> Runner.execute(%{})

expr =
~S|concat('x', "mid", 'z')|
|> Parser.parse_and_validate_expression!(refs: %{["mid"] => :text})

assert {:ok, "xbz"} = Runner.execute(expr, %{["mid"] => "b"})
assert {:ok, "xz"} = Runner.execute(expr, %{["mid"] => nil})

assert {:ok, true} =
~S|ilike(concat('%', "value", '%'), '%foo%')|
|> Parser.parse_and_validate_expression!(refs: %{["value"] => :text})
|> Runner.execute(%{["value"] => "foo"})

assert {:ok, nil} =
~S{'a' || NULL::text || 'b'}
|> Parser.parse_and_validate_expression!()
|> Runner.execute(%{})
end

test "concat() with zero arguments is not a supported overload" do
assert {:error, msg} = Parser.parse_and_validate_expression(~S|concat()|)
assert msg =~ "unknown or unsupported function concat/0"
end

test "subquery" do
assert {:ok, true} =
~S|test IN (SELECT val FROM tester)|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,23 @@ defmodule Electric.Replication.Eval.SqlGeneratorTest do
ast = %Func{name: "\"||\"", args: [%Ref{path: ["first"]}, %Ref{path: ["last"]}]}
assert SqlGenerator.to_sql(ast) == ~s("first" || "last")
end

test "concat variadic (parser Array wrapper)" do
ast = %Func{
name: "concat",
args: [
%Array{
elements: [
%Ref{path: ["first_name"]},
%Const{value: " "},
%Ref{path: ["last_name"]}
]
}
]
}

assert SqlGenerator.to_sql(ast) == ~s|concat("first_name", ' ', "last_name")|
end
end

describe "array operators" do
Expand Down