Skip to content

Commit 5cc2ebf

Browse files
committed
Fix non subquery conditions in tags
1 parent b7b165f commit 5cc2ebf

File tree

2 files changed

+100
-5
lines changed

2 files changed

+100
-5
lines changed

packages/sync-service/lib/electric/shapes/querying.ex

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule Electric.Shapes.Querying do
44
alias Electric.Shapes.Shape
55
alias Electric.Shapes.Shape.SubqueryMoves
66
alias Electric.Replication.Eval.Decomposer
7+
alias Electric.Replication.Eval.Parser.{Const, Ref, Func, Array}
78
alias Electric.Telemetry.OpenTelemetry
89

910
@value_prefix SubqueryMoves.value_prefix()
@@ -333,11 +334,102 @@ defmodule Electric.Shapes.Querying do
333334
end
334335

335336
# Generate SQL for a non-subquery condition
336-
# This is complex as we'd need to convert the AST back to SQL
337-
# For now, return true (since the row is in the result, the condition is satisfied)
338-
defp generate_non_subquery_condition_sql(_subexpr) do
339-
# Non-subquery conditions in the WHERE clause are already satisfied for returned rows
340-
# This is a simplification - for full accuracy we'd need AST-to-SQL conversion
337+
# Convert the AST back to SQL for accurate evaluation of OR conditions
338+
defp generate_non_subquery_condition_sql(subexpr) do
339+
ast_to_sql(subexpr.ast)
340+
end
341+
342+
# Convert AST to SQL - handles common comparison operators
343+
# Note: Operator names are stored with surrounding quotes (e.g., "\"=\"" for equals)
344+
defp ast_to_sql(%Func{name: "\"=\"", args: [left, right]}) do
345+
"(#{ast_to_sql(left)} = #{ast_to_sql(right)})"
346+
end
347+
348+
defp ast_to_sql(%Func{name: "\"<>\"", args: [left, right]}) do
349+
"(#{ast_to_sql(left)} <> #{ast_to_sql(right)})"
350+
end
351+
352+
defp ast_to_sql(%Func{name: "\"<\"", args: [left, right]}) do
353+
"(#{ast_to_sql(left)} < #{ast_to_sql(right)})"
354+
end
355+
356+
defp ast_to_sql(%Func{name: "\">\"", args: [left, right]}) do
357+
"(#{ast_to_sql(left)} > #{ast_to_sql(right)})"
358+
end
359+
360+
defp ast_to_sql(%Func{name: "\"<=\"", args: [left, right]}) do
361+
"(#{ast_to_sql(left)} <= #{ast_to_sql(right)})"
362+
end
363+
364+
defp ast_to_sql(%Func{name: "\">=\"", args: [left, right]}) do
365+
"(#{ast_to_sql(left)} >= #{ast_to_sql(right)})"
366+
end
367+
368+
defp ast_to_sql(%Func{name: "\"~~\"", args: [left, right]}) do
369+
"(#{ast_to_sql(left)} LIKE #{ast_to_sql(right)})"
370+
end
371+
372+
defp ast_to_sql(%Func{name: "\"~~*\"", args: [left, right]}) do
373+
"(#{ast_to_sql(left)} ILIKE #{ast_to_sql(right)})"
374+
end
375+
376+
defp ast_to_sql(%Func{name: "is null", args: [arg]}) do
377+
"(#{ast_to_sql(arg)} IS NULL)"
378+
end
379+
380+
defp ast_to_sql(%Func{name: "is not null", args: [arg]}) do
381+
"(#{ast_to_sql(arg)} IS NOT NULL)"
382+
end
383+
384+
defp ast_to_sql(%Func{name: "in", args: [left, %Array{elements: elements}]}) do
385+
values = Enum.map(elements, &ast_to_sql/1) |> Enum.join(", ")
386+
"(#{ast_to_sql(left)} IN (#{values}))"
387+
end
388+
389+
defp ast_to_sql(%Func{name: "not", args: [inner]}) do
390+
"(NOT #{ast_to_sql(inner)})"
391+
end
392+
393+
defp ast_to_sql(%Func{name: "and", args: args}) do
394+
conditions = Enum.map(args, &ast_to_sql/1) |> Enum.join(" AND ")
395+
"(#{conditions})"
396+
end
397+
398+
defp ast_to_sql(%Func{name: "or", args: args}) do
399+
conditions = Enum.map(args, &ast_to_sql/1) |> Enum.join(" OR ")
400+
"(#{conditions})"
401+
end
402+
403+
defp ast_to_sql(%Ref{path: path}) do
404+
# Quote the column name
405+
~s|"#{Enum.join(path, "\".\"")}"|
406+
end
407+
408+
defp ast_to_sql(%Const{value: nil}) do
409+
"NULL"
410+
end
411+
412+
defp ast_to_sql(%Const{value: value}) when is_binary(value) do
413+
# Escape single quotes in the value
414+
escaped = String.replace(value, "'", "''")
415+
"'#{escaped}'"
416+
end
417+
418+
defp ast_to_sql(%Const{value: value}) when is_integer(value) or is_float(value) do
419+
"#{value}"
420+
end
421+
422+
defp ast_to_sql(%Const{value: true}) do
423+
"true"
424+
end
425+
426+
defp ast_to_sql(%Const{value: false}) do
427+
"false"
428+
end
429+
430+
defp ast_to_sql(_other) do
431+
# Fallback for unsupported AST nodes - return true to avoid breaking
432+
# This is conservative: if we can't convert, assume the condition could be satisfied
341433
"true"
342434
end
343435

packages/sync-service/test/integration/active_conditions_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ defmodule Electric.Integration.ActiveConditionsTest do
430430
deleted_ids = Enum.map(deletes, & &1.value["id"]) |> Enum.sort()
431431
assert deleted_ids == ["child-1", "child-3"]
432432

433+
# Consume the up_to_date that follows the move-out processing
434+
assert_up_to_date(consumer)
435+
433436
# Verify child-2 was NOT deleted by checking we can still update it
434437
Postgrex.query!(
435438
db_conn,

0 commit comments

Comments
 (0)