zio.blocks.projection

Members list

Type members

Classlikes

Helpers for global aggregate projections.

Helpers for global aggregate projections.

Global projections share a single underlying store file (global/.db) and receive events from all sources regardless of entity routing. The entity identifier acts as the grouping key (for example a date string "2025-08-26").

The store layer guarantees that Increment / Decrement / Max / Min FieldUpdates translate to atomic SQL (COALESCE(col,0)+?, MAX(...), MIN(...)) and that a missing aggregate row is materialised on-the-fly via INSERT OR IGNORE before the UPDATE. Upsert uses the SQLite INSERT ... ON CONFLICT DO UPDATE form (falls back to INSERT OR REPLACE when the driver does not advertise conflict support).

Attributes

Source
AggregateProjection.scala
Supertypes
class Object
trait Matchable
class Any
Self type
case object BuildInfo

This object was generated by sbt-buildinfo.

This object was generated by sbt-buildinfo.

Attributes

Source
BuildInfo.scala
Supertypes
trait Singleton
trait Product
trait Mirror
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
Self type
BuildInfo.type
object EntityPath

Attributes

Companion
trait
Source
EntityPath.scala
Supertypes
class Object
trait Matchable
class Any
Self type
EntityPath.type
trait EntityPath[A]

Describes where a projection entity A lives on disk and which field is its identifier.

Describes where a projection entity A lives on disk and which field is its identifier.

basePath is the folder name (e.g. users derived from userId), entityIdField is the field name that holds the entity id.

Attributes

Companion
object
Source
EntityPath.scala
Supertypes
class Object
trait Matchable
class Any
final case class EventEnvelope[+E](seq: Long, tag: String, event: E, timestamp: Instant, entityId: String)

An event together with the metadata an EventStore records about it.

An event together with the metadata an EventStore records about it.

Envelopes are what an EventStore returns from readFrom and readAll, and what it publishes on its subscribe hub when a new event is appended. The ProjectionEngine reads them to run a projection's handlers: it passes event to the handler, along with a ProjectionContext built from entityId, timestamp and seq.

The type is covariant in E, so an EventEnvelope[UserCreated] can be used where an EventEnvelope[Any] is expected.

Type parameters

E

the event type

Value parameters

entityId

the entity the event was recorded under, which a handler sees as ctx.entityId

event

the event itself

seq

the position of the event in its store, increasing with each append. The engine saves the last seq it processed and resumes after it on restart

tag

the name that identifies the kind of event. The SQLite store uses the case name or the number from @eventTag, and replaces an old tag with its current one when tag aliases are configured. The in-memory test store uses the event class's simple name

timestamp

when the event was recorded

Attributes

Source
EventEnvelope.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
trait EventStore[E]

An append-only log of the events of one event type E.

An append-only log of the events of one event type E.

Each appended event gets the next sequence number, which also gives the order of the events. The store streams the events it holds from any position, and publishes every new event to a Hub, so that a reader can catch up on the history and then follow the new events. A ProjectionEngine reads each source of a projection from one event store.

SQLiteEventStore keeps the events in a SQLite file.

Type parameters

E

the event type, usually a sealed trait with one case for each kind of event

Attributes

Source
EventStore.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
class SQLiteEventStore[E]

Scala 3 macro implementations that turn a field selector such as _.userCount into the name of the field, checked at compile time.

Scala 3 macro implementations that turn a field selector such as _.userCount into the name of the field, checked at compile time.

They back the selector-based methods of FieldUpdate (increment, decrement, setValue, maxValue, minValue) and the HandlerBuilder.update macro, so that a misspelled field is a compile error and not a runtime failure. The selector must be a lambda that reads a field, such as _.userCount. Only the last field name of the selector is used, so _.stats.userCount gives userCount. A selector that is not a lambda, or that reads no field, such as x => x, is rejected with a compile error.

Field names are mapped to SQL snake_case column names with SqlNameMapper.SnakeCase, for example userCount becomes user_count. The name is worked out at compile time and appears as a string literal in the expanded code, so no reflection happens at runtime.

Attributes

Source
FieldSelectorMacros.scala
Supertypes
class Object
trait Matchable
class Any
Self type
object FieldUpdate

Attributes

Companion
enum
Source
FieldUpdate.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type

A sealed enumeration of field-level update operations.

A sealed enumeration of field-level update operations.

Each variant describes how a single field should be modified during an Update action on a ProjectionAction.

Provides two APIs:

  • '''String-based''' (direct): FieldUpdate.Increment("user_count", 1L)
  • '''Selector-based''' (macro): FieldUpdate.increment[User](_.userCount) which maps userCount → user_count via SqlNameMapper.SnakeCase

Attributes

Companion
object
Source
FieldUpdate.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
Known subtypes
class Set
class Increment
class Decrement
class Max
class Min
final class FromBuilder[A]

Chooses how the events of one named source are routed. It is returned by Projection.from, and each method returns the Projection with a new SourceBinding added. Handlers are attached afterwards with on.

Chooses how the events of one named source are routed. It is returned by Projection.from, and each method returns the Projection with a new SourceBinding added. Handlers are attached afterwards with on.

Attributes

Source
Projection.scala
Supertypes
class Object
trait Matchable
class Any
final case class Handler[E, A](eventClass: Class[E], schema: Schema[E], tag: String, fn: (E, ProjectionContext) => ProjectionAction[A])

Says what to do when an event of type E arrives: a function from the event and its ProjectionContext to a ProjectionAction on the projected type A.

Says what to do when an event of type E arrives: a function from the event and its ProjectionContext to a ProjectionAction on the projected type A.

Handlers are created by HandlerBuilder, not by hand. They contain no effects: a handler only describes the change, and the engine applies it.

Value parameters

eventClass

the runtime class used to recognise events this handler is for

fn

turns an event into the action to apply

schema

the schema of the event type

tag

the name of the event type, used in messages

Attributes

Source
Projection.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final class HandlerBuilder[E, A]

Says what an event of type E does to the projected type A. It is returned by Projection.on and is finished by calling one of its methods, each of which returns the Projection with the new handler added.

Says what an event of type E does to the projected type A. It is returned by Projection.on and is finished by calling one of its methods, each of which returns the Projection with the new handler added.

The methods fall into three groups:

  • whole rows: insert, delete
  • field changes: aggregate, update, updateWithField, updateField
  • anything else: custom

Attributes

Source
Projection.scala
Supertypes
class Object
trait Matchable
class Any
object Projection

Attributes

Companion
class
Source
Projection.scala
Supertypes
class Object
trait Matchable
class Any
Self type
Projection.type
final class Projection[A]

A declarative description of a read model A built from events: which sources to listen to, how to route their events, and what each kind of event does to A.

A declarative description of a read model A built from events: which sources to listen to, how to route their events, and what each kind of event does to A.

A Projection is immutable data and does nothing by itself. Every builder call returns a new Projection, and an engine (such as ProjectionEngine) later reads it to process events. Handlers only describe changes as ProjectionActions, so they cannot perform effects.

Create one with Projection[A](name) (one row per entity) or Projection.global[A](name) (shared totals), then chain from, a routing call, and on:

val greetings = Projection[Greeting]("greetings")
 .from("people")
 .routeToSelf
 .on[PersonGreeted]
 .insert((e, ctx) => Greeting(ctx.entityId, s"Hello, ${e.name}!"))

Further from(...) calls add more sources, and further on[...] calls add more handlers to the latest source. An on[...] with no from(...) before it creates a default source named "_default" that routes to the entity itself.

Call validate before starting an engine to catch an empty projection.

Value parameters

bindings

the sources with their routing and handlers, in the order they were added

entityPath

where the store lives and which field of A is its ID. It is None for global projections

isGlobal

whether this is a global projection

name

the projection's name, which also names its store file

schema

the schema of the projected type A

Attributes

Companion
object
Source
Projection.scala
Supertypes
class Object
trait Matchable
class Any

Attributes

Companion
enum
Source
ProjectionAction.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
enum ProjectionAction[+A]

A sealed enumeration of actions that can be applied to a projection.

A sealed enumeration of actions that can be applied to a projection.

ProjectionAction[+A] is covariant in A, allowing actions on subtypes to be used where supertype actions are expected.

Each variant represents a distinct operation on projected data:

  • Insert: Add a new entity
  • Upsert: Add or update an entity
  • Update: Modify specific fields of an existing entity
  • Delete: Remove an entity (sentinel — no payload)
  • Truncate: Remove all entities (sentinel — no payload)
  • Noop: No operation (sentinel — no payload)

Type parameters

A

The entity type this action operates on

Attributes

Companion
object
Source
ProjectionAction.scala
Supertypes
trait Enum
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
Known subtypes
class Insert[A]
class Upsert[A]
class Update[A]
final case class ProjectionContext(entityId: String, timestamp: Instant, seq: Long, sourceEntityId: Option[String])

Contextual metadata associated with a projection action.

Contextual metadata associated with a projection action.

Value parameters

entityId

the identifier of the entity this action targets

seq

a monotonically increasing sequence number for ordering

sourceEntityId

optional identifier of the entity in the source system

timestamp

the time the action was produced

Attributes

Companion
object
Source
ProjectionContext.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Companion
class
Source
ProjectionContext.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final class ProjectionEngine

Runs projections: it reads events from EventStores, applies each Projection's handlers, and keeps the results in ProjectionStores that can be queried.

Runs projections: it reads events from EventStores, applies each Projection's handlers, and keeps the results in ProjectionStores that can be queried.

A Projection only describes what should happen. The engine is the part that does it. Each projection gets a primary store, which is one SQLite file per projection on the JVM or an in-memory map in tests.

Lifecycle: start first compares each projection's stored schema hash with the current one, and brings stale stores up to date unless lazyRebuild is on. Then, for each source of each projection, it catches up on the events after the projection's saved position, reading them in batches with readFrom, and only then starts a fiber that follows the new events from the source's hub. start returns once every source has caught up. query first applies any pending lazy rebuild.

Routing: for each event the engine builds a ProjectionContext from the EventEnvelope, runs the matching handler, and applies the resulting ProjectionAction to the store chosen by the projection's ProjectionScope: the primary store for per-entity and global projections, and a shard per routing key for cross-entity ones.

Failures: an event whose handler throws, or that the store rejects, is logged as an error and skipped; an event that no handler matches is skipped too, and logged at debug level. Processing continues with the next event. The saved position is updated after each event that succeeds, so a skipped event is not retried once a later one succeeds.

Persistence: the stores that make, makeWithConfig and makeWithStoresVarargs create are SQLite files, so sqlite-jdbc must be on the runtime classpath and the folders of the files must exist. A store opens its file only when it is first used, so a missing driver or folder shows up as a failure then, not when the engine is created.

Create an engine with the factories on the companion object. make and makeWithConfig give it no event stores, so it has nothing to read; to give it event stores, create it with makeWithStores or makeWithStoresVarargs, or use TestEngine in tests.

Value parameters

cache

shared TransactorCache for SQLite connections

config

the settings the engine was created with

eventStores

per-source event stores keyed by source name

evolutionSem

semaphore guarding rebuilds

migrationRegistry

registered schema migrations

pendingRebuildRef

specs pending lazy rebuild

primaryStores

per-spec primary stores keyed by spec name

shardsRef

sharded stores for cross-entity routing

specs

list of projections this engine manages

Attributes

Companion
object
Source
ProjectionEngine.scala
Supertypes
class Object
trait Matchable
class Any

Attributes

Companion
class
Source
ProjectionEngine.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Attributes

Companion
class
Source
ProjectionEngine.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class ProjectionEngineConfig(batchSize: Int = ..., batchTimeout: Duration = ..., ringCapacity: Int = ..., rebuildParallelism: Int = ..., lazyRebuild: Boolean = ..., evolution: SchemaEvolutionConfig = ...)

Settings for a ProjectionEngine.

Settings for a ProjectionEngine.

Value parameters

batchSize

the most events read together while catching up on history. Each event of a batch is still applied, and its position saved, on its own.

batchTimeout

how long catch-up waits for a batch to fill before it applies what it has

evolution

settings for SchemaEvolution. The engine does not read them: it always tries the migration shortcut when a migration is registered for the projection, and uses its own lazyRebuild and rebuildParallelism.

lazyRebuild

if true, a projection whose schema changed is only marked as pending at startup, and rebuilt on its first query or by the background pre-warm. If false, the default, it is rebuilt during start.

rebuildParallelism

the size of the groups in which the background pre-warm rebuilds pending projections when lazyRebuild is on. The groups, and the projections in them, are rebuilt one after the other.

ringCapacity

the capacity of the bounded hub that carries live events, where a hub is created from the config, as TestEngine does. An event store made with SQLiteEventStore.make has its own hub of 4,096 events.

Attributes

Companion
object
Source
ProjectionEngine.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all

Attributes

Source
ProjectionMacros.scala
Supertypes
class Object
trait Matchable
class Any
Self type
sealed trait ProjectionScope

How a Projection's data is split up. It is never set directly: see Projection.scope for how it is derived from the way the projection was built. The engine uses it to choose the store an event goes to and the file the store lives in.

How a Projection's data is split up. It is never set directly: see Projection.scope for how it is derived from the way the projection was built. The engine uses it to choose the store an event goes to and the file the store lives in.

Attributes

Companion
object
Source
Projection.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
class CrossEntity
object Global
object PerEntity

Attributes

Companion
trait
Source
Projection.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
trait ProjectionStore[A]

Where the rows of one read model A are kept.

Where the rows of one read model A are kept.

A store holds the rows, each under the entity ID read from its ID field (see EntityPath), and two values the engine keeps next to them: the watermark, which is the sequence number of the last event applied to the rows, and the schema hash, a fingerprint of the shape of A that tells the engine when the rows are out of date.

The engine applies the ProjectionAction that a handler returns with one of these methods, and reads a row back with findById. SQLiteProjectionStore keeps the rows in a SQLite file, and testing.InMemoryProjectionStore keeps them in memory, for tests.

Type parameters

A

the type of a row: a case class with an ID field

Attributes

Source
ProjectionStore.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
sealed trait RoutingMode

Decides which row or shard of a projection an incoming event is applied to.

Decides which row or shard of a projection an incoming event is applied to.

A routing mode belongs to a SourceBinding and is chosen on FromBuilder with routeToSelf, routeToAll or routedBy. The mode produces a ''routing key'' for each event (see Projection.routingKey), and it also determines the projection's ProjectionScope.

Attributes

Companion
object
Source
Projection.scala
Supertypes
class Object
trait Matchable
class Any
Known subtypes
object RouteToAll
object RouteToSelf
class RoutedBy[E]
object RoutingMode

Attributes

Companion
trait
Source
Projection.scala
Supertypes
trait Sum
trait Mirror
class Object
trait Matchable
class Any
Self type
final class SQLiteEventStore[E] extends EventStore[E]

An EventStore kept in a SQLite database.

An EventStore kept in a SQLite database.

Each event is a row of the events table, with the columns seq, tag, payload, ts and entityId. seq is the primary key and gives the sequence number. tag is the @eventTag number of the event's case, or else the case's name; for an event type that is not a sealed trait, it is the type's own @eventTag number or name. payload is the event encoded as JSON, and ts is the time it was appended, in milliseconds. An index on tag and seq serves filtered reads.

The store knows the tags of E through a TagInfo. When a case was renamed, a read that asks for the new tag also returns the events saved under the old one, decodes them as the renamed case, and reports them under the new tag.

A read opens its own connection and pulls the rows through a cursor as the stream is consumed. An event whose payload cannot be decoded fails the stream.

The constructor is private: create a store with the factories of the companion object, which also create the table and its index if they are missing.

Value parameters

subscribe

the hub that every appended event is published to

tagInfo

the tags of E, and the old tags that were renamed to them

Attributes

Companion
object
Source
EventStore.scala
Supertypes
trait EventStore[E]
class Object
trait Matchable
class Any

Attributes

Companion
class
Source
EventStore.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Attributes

Companion
class
Source
SQLiteProjectionStore.scala
Supertypes
class Object
trait Matchable
class Any
Self type

A ProjectionStore kept in a SQLite file. ProjectionEngine.make creates one for each projection; with ProjectionEngine.makeWithStores you create them yourself.

A ProjectionStore kept in a SQLite file. ProjectionEngine.make creates one for each projection; with ProjectionEngine.makeWithStores you create them yourself.

The rows are kept in a table named after A, with one column for each field, in snake_case, and a unique index on the ID column. The watermark and the schema hash are kept in a table called _projection_meta, in a row named after the rows' table. The tables are created the first time the store is used, not when it is made.

upsert uses INSERT ... ON CONFLICT DO UPDATE, and falls back to INSERT OR REPLACE if SQLite rejects that statement. updateFields checks every field name against the table's columns before it starts, then runs INSERT OR IGNORE for the default row and one UPDATE for each field update, in one transaction.

The constructor is private: create a store with SQLiteProjectionStore.make.

Type parameters

A

the type of a row, with an EntityPath that names its ID field

Attributes

Companion
object
Source
SQLiteProjectionStore.scala
Supertypes
trait ProjectionStore[A]
class Object
trait Matchable
class Any

Detects schema drift via SchemaHash and rebuilds or migrates projection stores.

Detects schema drift via SchemaHash and rebuilds or migrates projection stores.

Attributes

Source
SchemaEvolution.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Attributes

Companion
class
Source
SchemaEvolution.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class SchemaEvolutionConfig(rebuildParallelism: Int = ..., lazyRebuild: Boolean = ..., enableMigrationShortcut: Boolean = ...)

Controls schema-evolution behavior (rebuild vs migration shortcut).

Controls schema-evolution behavior (rebuild vs migration shortcut).

Attributes

Companion
object
Source
SchemaEvolution.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object SchemaHash

SHA-256 hash of a schema's structure for evolution detection.

SHA-256 hash of a schema's structure for evolution detection.

Records recurse into field schemas (not just type names) so nested changes affect the hash.

Attributes

Source
SchemaHash.scala
Supertypes
class Object
trait Matchable
class Any
Self type
SchemaHash.type
final case class SourceBinding[E, A](sourceName: String, routing: RoutingMode, handlers: List[Handler[_, A]])

The rules a Projection applies to one named event source: how its events are routed, and which handlers react to them.

The rules a Projection applies to one named event source: how its events are routed, and which handlers react to them.

A binding is added by from(sourceName) followed by a routing call, and each on[E] handler is then attached to the most recently added binding. The type parameter E is not used by any field.

Value parameters

handlers

the handlers for this source, tried in the order they were added

routing

how events from this source are routed

sourceName

the name of the event source, as given to Projection.from

Attributes

Source
Projection.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final case class TagInfo(aliases: Map[String, String], allTags: Set[String], currentTags: Set[String], migration: DynamicMigration)

Which event tags exist for an event type, and how old tags map to current ones.

Which event tags exist for an event type, and how old tags map to current ones.

Event tags are stored as strings, and a renamed case leaves old events in the store under its old tag. A TagInfo records which tags are current, which are old aliases of them, and the migration that rewrites old events. An EventStore uses it to read old events as if they carried the current tag.

Build one with TagResolver rather than by hand.

Value parameters

aliases

map from an old tag to the current tag it was renamed to. Chains of renames are already followed, so every old tag maps straight to its final name

allTags

every tag that may legitimately be in the store: the current tags plus all old ones

currentTags

the tags the event type's schema produces now

migration

the migration used to transform old events

Attributes

Source
TagResolver.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
object TagResolver

Works out the tags of an event type and the aliases left behind by renamed cases. It reads the current tags from Schema[E], and the old ones from the RenameCase actions of an optional Migration[E, E].

Works out the tags of an event type and the aliases left behind by renamed cases. It reads the current tags from Schema[E], and the old ones from the RenameCase actions of an optional Migration[E, E].

Without a migration there are no aliases. With one, each renamed case becomes an alias, so old events can still be read and queried. The names are used exactly as written in the migration.

val migration: Migration[UserEvent, UserEvent] =
 Migration.newBuilder[UserEvent, UserEvent]
   .renameCase("UserCreated", "UserRegistered")
   .build

val info = TagResolver.resolve[UserEvent](migration)
// info.aliases == Map("UserCreated" -> "UserRegistered")

The event stores call these functions when they are created, so applications normally only pass a migration and do not call them directly.

Attributes

Companion
trait
Source
TagResolver.scala
Supertypes
class Object
trait Matchable
class Any
Self type
trait TagResolver[E]

A view of the TagInfo for an event type E, with its parts as methods.

A view of the TagInfo for an event type E, with its parts as methods.

Instances are made by implementing tagInfo. Most code uses the functions on the companion object to get a TagInfo directly.

Attributes

Companion
object
Source
TagResolver.scala
Supertypes
class Object
trait Matchable
class Any

Attributes

Companion
trait
Source
TransactorCache.scala
Supertypes
class Object
trait Matchable
class Any
Self type

Cache of SQLite Transactors keyed by file path with LRU eviction.

Cache of SQLite Transactors keyed by file path with LRU eviction.

Each distinct file path maps to a single Transactor instance. When the cache reaches maxSize the least-recently-used entry is evicted.

Attributes

Companion
object
Source
TransactorCache.scala
Supertypes
class Object
trait Matchable
class Any

Attributes

Companion
class
Source
TransactorCache.scala
Supertypes
trait Product
trait Mirror
class Object
trait Matchable
class Any
Self type
final case class TransactorCacheConfig(maxSize: Int = ...)

Configuration for TransactorCache.

Configuration for TransactorCache.

Attributes

Companion
object
Source
TransactorCache.scala
Supertypes
trait Serializable
trait Product
trait Equals
class Object
trait Matchable
class Any
Show all
final class eventTag(val value: Int) extends StaticAnnotation

Gives an event case a numeric tag in place of its default name-based tag.

Gives an event case a numeric tag in place of its default name-based tag.

Every event an EventStore appends is stored with a tag that identifies its kind, and reads can be filtered by tag. By default the tag is the case name, so renaming a case changes the tag that new events are stored with. Annotating the case with @eventTag(n) makes the tag the string form of n instead, for example "1", which stays the same when the case is renamed.

Put the annotation on each case class of a sealed event type, with a different number for each case. The numbers are not checked for uniqueness. It is also honoured on a single, non-sealed event type.

sealed trait UserEvent

@eventTag(1)
case class Created(userId: String, name: String) extends UserEvent

@eventTag(2)
case class Deleted(userId: String) extends UserEvent

The tag is the event's identity in the store's tag column and in EventEnvelope. It does not change how the event's payload is encoded.

Value parameters

value

the number to use as the tag

Attributes

Source
EventTag.scala
Supertypes
trait StaticAnnotation
class Annotation
class Object
trait Matchable
class Any
class path(name: String) extends StaticAnnotation

Attributes

Source
EntityPath.scala
Supertypes
trait StaticAnnotation
class Annotation
class Object
trait Matchable
class Any