Sourced.EventStore.Postgres.Event (sourced_postgres v0.3.1)

Copy Markdown View Source

Ecto schema for the sourced events table, and every query the adapter runs against it.

Queries live here rather than beside the repo calls that run them, so Ecto.Query is imported in one place and the read path and the append condition cannot drift apart on what counts as a match.

Summary

Functions

Query asking whether anything matching query has arrived past expected_sequence.

Query returning the sequence of the last event matching query past expected_sequence, or 0 when nothing is past it.

Query returning the sequence of the last event matching query, or 0 when nothing matches.

Query over the events matching query, or every event when it is nil or empty.

Functions

conflict_query(query, expected_sequence)

Query asking whether anything matching query has arrived past expected_sequence.

This is the DCB check, and under SERIALIZABLE it needs no help to be race-free: the read takes SIREAD predicate locks over the range it touched, a concurrent insert into that range creates an rw-conflict, and the dangerous structure detector aborts one side. It needs no ordering against the insert that follows it, and so runs as an ordinary statement of its own.

Phrased as an existence check over the tail rather than as max(sequence) over everything matching, because under SSI the read set is the lock set: this reads only past expected_sequence and can stop at the first row, which is a strictly narrower predicate-lock footprint for an equivalent question.

conflicting_sequence(query, expected_sequence)

Query returning the sequence of the last event matching query past expected_sequence, or 0 when nothing is past it.

Reports what a rejected append conflicted with. Bounded to the same tail as conflict_query/2, so it is only meaningful where something is already known to be in that tail — the 0 it returns otherwise would misreport an empty tail as an empty stream. Where that is not known, use last_sequence/1.

Worth the narrowing because this runs inside the append's own transaction, where a read of the whole matched range would take the wide predicate locks that conflict_query/2 exists to avoid.

last_sequence(query)

Query returning the sequence of the last event matching query, or 0 when nothing matches.

read_query(opts)

Query over the events matching query, or every event when it is nil or empty.

:from and :to bound the result to sequences at or past, and at or below, them. Each bound is applied twice, once per table it serves: on the events table, where it is a range on the primary key and the only bound there is when no subquery is built; and inside the subquery on the tags table, where sourced_event_tags is keyed by (tag, sequence) and it makes a read of a tag a range scan rather than everything ever tagged. Neither stands in for the other: Postgres does not push an inequality into a subquery, nor infer one on the outer table from the values a subquery returns, and the grouping below materializes the whole candidate set before the outer query sees any of it.

The items are not combined with OR. Postgres only plans sequence IN (subquery) as a semi-join when it is a top-level condition; under an OR it becomes a subplan evaluated against every row, which on a conflict check from sequence 0 is a sequential scan of the whole table — and under SERIALIZABLE a predicate lock on all of it. Instead the items are rows of a VALUES list joined to the tags table, so any number of them resolve in one pass over the tag index, and the outer query sees a single IN.

with_query(opts)