Skip to content

Commit 68ba7ed

Browse files
authored
fix: account for ETS matching maps when comparing shapes (#2824)
1 parent 7e8c4f4 commit 68ba7ed

5 files changed

Lines changed: 46 additions & 13 deletions

File tree

.changeset/serious-cats-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@core/sync-service": patch
3+
---
4+
5+
fix: ensure correct shape lookup & comparison

packages/sync-service/lib/electric/replication/eval/expr.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ defmodule Electric.Replication.Eval.Expr do
7070

7171
defimpl Electric.Shapes.Shape.Comparable do
7272
def comparable(%Electric.Replication.Eval.Expr{} = expr) do
73-
%{expr | eval: Electric.Shapes.Shape.Comparable.comparable(expr.eval)}
73+
{:eval_expr, expr.query, expr.returns}
7474
end
7575
end
7676
end

packages/sync-service/lib/electric/shape_cache/shape_status.ex

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,11 @@ defmodule Electric.ShapeCache.ShapeStatus do
179179
end
180180

181181
def get_existing_shape(meta_table, %Shape{} = shape) do
182-
case :ets.select(meta_table, [
183-
{{{@shape_hash_lookup, Shape.comparable(shape)}, :"$1"}, [true], [:"$1"]}
184-
]) do
185-
[] ->
182+
case :ets.lookup_element(meta_table, {@shape_hash_lookup, Shape.comparable(shape)}, 2, nil) do
183+
nil ->
186184
nil
187185

188-
[shape_handle] ->
186+
shape_handle when is_binary(shape_handle) ->
189187
try do
190188
{shape_handle, latest_offset!(meta_table, shape_handle)}
191189
rescue

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ defmodule Electric.Shapes.Shape do
1111
require Logger
1212

1313
defprotocol Comparable do
14+
@fallback_to_any true
15+
1416
@spec comparable(t()) :: t()
1517
def comparable(term)
1618
end
1719

20+
defimpl Comparable, for: Any do
21+
def comparable(term), do: term
22+
end
23+
1824
@default_replica :default
1925

2026
@enforce_keys [:root_table, :root_table_id]
@@ -67,14 +73,21 @@ defmodule Electric.Shapes.Shape do
6773
storage: %{required(String.t()) => String.t()}
6874
}
6975

76+
@doc """
77+
Return a comparable representation of the shape.
78+
79+
This is used to compare shapes for equality as an ETS key - and thus it'll be matched in some cases,
80+
not just compared equal. This representation must therefore not contain any maps (as they are matched when one
81+
is missing a key for example).
82+
83+
This representation must contain all the information that identifies user-specified properties of the shape. We're
84+
omitting storage configuration and other internal state.
85+
"""
7086
def comparable(%__MODULE__{} = shape) do
71-
shape
72-
|> Map.drop([:table_info, :storage])
73-
|> Map.update!(:where, fn
74-
nil -> nil
75-
%Expr{} = expr -> Electric.Shapes.Shape.Comparable.comparable(expr)
76-
statement when is_binary(statement) -> statement
77-
end)
87+
{:shape, {shape.root_table_id, shape.root_table}, shape.root_pk,
88+
Comparable.comparable(shape.where), shape.selected_columns,
89+
Enum.flat_map(shape.flags, fn {k, v} -> if(v, do: [k], else: []) end) |> Enum.sort(),
90+
shape.replica}
7891
end
7992

8093
def hash(%__MODULE__{} = shape),

packages/sync-service/test/electric/shape_cache_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,23 @@ defmodule Electric.ShapeCacheTest do
127127

128128
assert shape_handle1 != shape_handle2
129129
end
130+
131+
test "should not return the same shape_handle for all columns and selected columns", %{
132+
shape_cache_opts: opts
133+
} do
134+
alias Electric.Replication.Eval.Parser
135+
136+
shape1 = @shape
137+
138+
shape2 = Shape.new!("items", inspector: @stub_inspector, columns: ["id", "value"])
139+
140+
{shape_handle2, @zero_offset} = ShapeCache.get_or_create_shape_handle(shape2, opts)
141+
{shape_handle1, @zero_offset} = ShapeCache.get_or_create_shape_handle(shape1, opts)
142+
143+
assert shape_handle1 != shape_handle2
144+
145+
assert {^shape_handle2, @zero_offset} = ShapeCache.get_or_create_shape_handle(shape2, opts)
146+
end
130147
end
131148

132149
describe "get_or_create_shape_handle/2 shape initialization" do

0 commit comments

Comments
 (0)