Packages
electric
1.4.6
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/electric/shapes/shape/subquery_moves.ex
defmodule Electric.Shapes.Shape.SubqueryMoves do
@moduledoc false
alias Electric.Replication.Eval
alias Electric.Replication.Eval.Walker
alias Electric.Shapes.Shape
@value_prefix "v:"
@null_sentinel "NULL"
def value_prefix, do: @value_prefix
def null_sentinel, do: @null_sentinel
@doc """
Given a shape with a where clause that contains a subquery, make a query that can use a
list of value in place of the subquery.
When we're querying for new data, we're only querying for a subset of entire query.
To make that, we need to replace the subquery with a list of values.
For example, if the shape has a where clause like this:
~S|WHERE parent_id IN (SELECT id FROM parent WHERE value = '1')|
And we're querying for new data with a list of values like this:
["1", "2", "3"]
Then the query will be transformed to:
~S|WHERE parent_id = ANY ($1::text[]::int8[])|
And the parameters will be:
[["1", "2", "3"]]
"""
def move_in_where_clause(
%Shape{
where: %{query: query, used_refs: used_refs},
shape_dependencies: shape_dependencies,
shape_dependencies_handles: shape_dependencies_handles
},
shape_handle,
move_ins
) do
index = Enum.find_index(shape_dependencies_handles, &(&1 == shape_handle))
target_section = Enum.at(shape_dependencies, index) |> rebuild_subquery_section()
case used_refs[["$sublink", "#{index}"]] do
{:array, {:row, cols}} ->
unnest_sections =
cols
|> Enum.map(&Electric.Replication.Eval.type_to_pg_cast/1)
|> Enum.with_index(fn col, index -> "$#{index + 1}::text[]::#{col}[]" end)
|> Enum.join(", ")
{String.replace(query, target_section, "IN (SELECT * FROM unnest(#{unnest_sections}))"),
Electric.Utils.unzip_any(move_ins) |> Tuple.to_list()}
col ->
type = Electric.Replication.Eval.type_to_pg_cast(col)
{String.replace(query, target_section, "= ANY ($1::text[]::#{type})"), [move_ins]}
end
end
defp rebuild_subquery_section(shape) do
base =
~s|IN (SELECT #{Enum.join(shape.explicitly_selected_columns, ", ")} FROM #{Electric.Utils.relation_to_sql(shape.root_table)}|
where = if shape.where, do: " WHERE #{shape.where.query}", else: ""
base <> where <> ")"
end
@doc """
Generate a tag-removal control message for a shape.
Patterns are a list of lists, where each inner list represents a pattern (and is functionally a tuple, but
JSON can't directly represent tuples). This pattern is filled with actual values that have been removed.
"""
@spec make_move_out_control_message(Shape.t(), String.t(), String.t(), [
{dep_handle :: String.t(), gone_values :: String.t()},
...
]) :: map()
# Stub guard to allow only one dependency for now.
def make_move_out_control_message(shape, stack_id, shape_handle, [_] = move_outs) do
%{
headers: %{
event: "move-out",
patterns:
Enum.flat_map(move_outs, &make_move_out_pattern(shape, stack_id, shape_handle, &1))
}
}
end
# This is a stub implementation valid only for when there is exactly one dependency.
defp make_move_out_pattern(
%{tag_structure: patterns},
stack_id,
shape_handle,
{_dep_handle, gone_values}
) do
# TODO: This makes the assumption of only one column per pattern.
Enum.flat_map(patterns, fn [column_or_expr] ->
case column_or_expr do
column_name when is_binary(column_name) ->
Enum.map(
gone_values,
&%{pos: 0, value: make_value_hash(stack_id, shape_handle, elem(&1, 1))}
)
{:hash_together, columns} ->
column_parts =
&(Enum.zip_with(&1, columns, fn value, column ->
column <> ":" <> namespace_value(value)
end)
|> Enum.join())
Enum.map(
gone_values,
&%{
pos: 0,
value:
make_value_hash_raw(
stack_id,
shape_handle,
column_parts.(Tuple.to_list(elem(&1, 1)))
)
}
)
end
end)
end
def make_value_hash(stack_id, shape_handle, value) do
make_value_hash_raw(stack_id, shape_handle, namespace_value(value))
end
@doc """
Hash a pre-namespaced value. Use `make_value_hash/3` for single values that need namespacing.
"""
def make_value_hash_raw(stack_id, shape_handle, namespaced_value) do
:crypto.hash(:md5, "#{stack_id}#{shape_handle}#{namespaced_value}")
|> Base.encode16(case: :lower)
end
@doc """
Namespace a value for hashing.
To distinguish NULL from the literal string 'NULL', values are prefixed with
'v:' and NULL becomes 'NULL' (no prefix). This MUST match the SQL logic in
`Querying.pg_namespace_value_sql/1` - see lib/electric/shapes/querying.ex.
"""
def namespace_value(nil), do: @null_sentinel
def namespace_value(value), do: @value_prefix <> value
@doc """
Generate a tag structure for a shape.
A tag structure is a list of lists, where each inner list represents a tag (and is functionally a tuple, but
JSON can't directly represent tuples). The structure is used to generate actual tags for each row, that act
as a refenence as to why this row is part of the shape.
Tag structure then is essentially a list of column names in correct positions that will get filled in
with actual values from the row
"""
@spec move_in_tag_structure(Shape.t()) ::
list(list(String.t() | {:hash_together, [String.t(), ...]}))
def move_in_tag_structure(%Shape{} = shape)
when is_nil(shape.where)
when shape.shape_dependencies == [],
do: {[], %{}}
def move_in_tag_structure(shape) do
# TODO: For multiple subqueries this should be a DNF form
# and this walking overrides the comparison expressions
{:ok, {tag_structure, comparison_expressions}} =
Walker.reduce(
shape.where.eval,
fn
%Eval.Parser.Func{name: "sublink_membership_check", args: [testexpr, sublink_ref]},
{[current_tag | others], comparison_expressions},
_ ->
tags =
case testexpr do
%Eval.Parser.Ref{path: [column_name]} ->
[[column_name | current_tag] | others]
%Eval.Parser.RowExpr{elements: elements} ->
elements =
Enum.map(elements, fn %Eval.Parser.Ref{path: [column_name]} ->
column_name
end)
[[{:hash_together, elements} | current_tag] | others]
end
{:ok, {tags, Map.put(comparison_expressions, sublink_ref.path, testexpr)}}
_, acc, _ ->
{:ok, acc}
end,
{[[]], %{}}
)
comparison_expressions
|> Map.new(fn {path, expr} -> {path, Eval.Expr.wrap_parser_part(expr)} end)
|> then(&{tag_structure, &1})
end
end