Current section

Files

Jump to
eventstore lib event_store storage appender.ex
Raw

lib/event_store/storage/appender.ex

defmodule EventStore.Storage.Appender do
@moduledoc false
require Logger
alias EventStore.RecordedEvent
alias EventStore.Sql.Statements
@all_stream_id 0
@doc """
Append the given list of events to storage.
Events are inserted in batches of 1,000 within a single transaction. This is
due to PostgreSQL's limit of 65,535 parameters for a single statement.
Returns `:ok` on success, `{:error, reason}` on failure.
"""
def append(conn, stream_id, events, opts) do
{schema, opts} = Keyword.pop(opts, :schema)
stream_uuid = stream_uuid(events)
try do
events
|> Stream.map(&encode_uuids/1)
|> Stream.chunk_every(1_000)
|> Enum.each(fn batch ->
event_count = length(batch)
stream_params =
Enum.flat_map(batch, fn
%RecordedEvent{event_id: event_id, stream_version: stream_version} ->
[event_id, stream_version]
end)
all_stream_params =
batch
|> Stream.with_index(1)
|> Enum.flat_map(fn {%RecordedEvent{event_id: event_id}, index} ->
[index, event_id]
end)
stream_params = [stream_id | [event_count | stream_params]]
all_stream_params = [@all_stream_id | [event_count | all_stream_params]]
with :ok <- insert_event_batch(conn, batch, schema, opts),
:ok <- insert_stream_events(conn, stream_params, event_count, schema, opts),
:ok <- insert_link_events(conn, all_stream_params, event_count, schema, opts) do
Logger.debug("Appended #{length(events)} event(s) to stream #{inspect(stream_uuid)}")
:ok
else
{:error, error} -> throw({:error, error})
end
end)
catch
{:error, error} = reply ->
Logger.warn(
"Failed to append events to stream #{inspect(stream_uuid)} due to: " <> inspect(error)
)
reply
end
end
@doc """
Link the given list of existing event ids to another stream in storage.
Returns `:ok` on success, `{:error, reason}` on failure.
"""
def link(conn, stream_id, event_ids, opts \\ [])
def link(conn, stream_id, event_ids, opts) do
{schema, opts} = Keyword.pop(opts, :schema)
try do
event_ids
|> Stream.map(&encode_uuid/1)
|> Stream.chunk_every(1_000)
|> Enum.each(fn batch ->
event_count = length(batch)
parameters =
batch
|> Stream.with_index(1)
|> Enum.flat_map(fn {event_id, index} -> [index, event_id] end)
params = [stream_id | [event_count | parameters]]
with :ok <- insert_link_events(conn, params, event_count, schema, opts) do
Logger.debug("Linked #{length(event_ids)} event(s) to stream")
:ok
else
{:error, error} -> throw({:error, error})
end
end)
catch
{:error, error} = reply ->
Logger.warn("Failed to link events to stream due to: #{inspect(error)}")
reply
end
end
defp encode_uuids(%RecordedEvent{} = event) do
%RecordedEvent{
event
| event_id: event.event_id |> uuid(),
causation_id: event.causation_id |> uuid(),
correlation_id: event.correlation_id |> uuid()
}
end
defp encode_uuid(event_id) when is_bitstring(event_id) do
event_id |> uuid()
end
defp insert_event_batch(conn, events, schema, opts) do
event_count = length(events)
statement = Statements.insert_events(schema, event_count)
parameters = build_insert_parameters(events)
conn
|> Postgrex.query(statement, parameters, opts)
|> handle_response()
end
defp build_insert_parameters(events) do
events
|> Enum.flat_map(fn event ->
[
event.event_id,
event.event_type,
event.causation_id,
event.correlation_id,
event.data,
event.metadata,
event.created_at
]
end)
end
defp insert_stream_events(conn, params, event_count, schema, opts) do
statement = Statements.insert_stream_events(schema, event_count)
conn
|> Postgrex.query(statement, params, opts)
|> handle_response()
end
defp insert_link_events(conn, params, event_count, schema, opts) do
statement = Statements.insert_link_events(schema, event_count)
conn
|> Postgrex.query(statement, params, opts)
|> handle_response()
end
defp uuid(nil), do: nil
defp uuid(uuid), do: UUID.string_to_binary!(uuid)
defp handle_response({:ok, %Postgrex.Result{num_rows: 0}}), do: {:error, :not_found}
defp handle_response({:ok, %Postgrex.Result{}}), do: :ok
defp handle_response({:error, %Postgrex.Error{} = error}) do
%Postgrex.Error{postgres: %{code: error_code, constraint: constraint}} = error
case {error_code, constraint} do
{:foreign_key_violation, _constraint} -> {:error, :not_found}
{:unique_violation, "events_pkey"} -> {:error, :duplicate_event}
{:unique_violation, "stream_events_pkey"} -> {:error, :duplicate_event}
{:unique_violation, _constraint} -> {:error, :wrong_expected_version}
{error_code, _constraint} -> {:error, error_code}
end
end
defp stream_uuid([event | _]), do: event.stream_uuid
end