# Materializers

Materializers are functions that allow you to write to your database in response to events. Materializers are executed in the order of the events in the eventlog.

## Example


## `reference/state/materializers/example.ts`

```ts filename="reference/state/materializers/example.ts"
import { defineMaterializer, Events, Schema, State } from '@livestore/livestore'

export const todos = State.SQLite.table({
  name: 'todos',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    text: State.SQLite.text(),
    completed: State.SQLite.boolean({ default: false }),
    previousIds: State.SQLite.json({
      schema: Schema.Array(Schema.String),
      nullable: true,
    }),
  },
})

export const table1 = State.SQLite.table({
  name: 'settings',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    someVal: State.SQLite.integer({ default: 0 }),
  },
})

export const table2 = State.SQLite.table({
  name: 'preferences',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    otherVal: State.SQLite.text({ default: 'default' }),
  },
})

export const events = {
  todoCreated: Events.synced({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean.pipe(Schema.optional),
    }),
  }),
  userPreferencesUpdated: Events.synced({
    name: 'userPreferencesUpdated',
    schema: Schema.Struct({ userId: Schema.String, theme: Schema.String }),
  }),
  factoryResetApplied: Events.synced({
    name: 'factoryResetApplied',
    schema: Schema.Struct({}),
  }),
} as const

export const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, completed }) =>
    todos.insert({ id, text, completed: completed ?? false }),
  ),
  [events.userPreferencesUpdated.name]: defineMaterializer(events.userPreferencesUpdated, ({ userId, theme }) => {
    console.log(`User ${userId} updated theme to ${theme}.`)
    return []
  }),
  [events.factoryResetApplied.name]: defineMaterializer(events.factoryResetApplied, () => [
    table1.update({ someVal: 0 }),
    table2.update({ otherVal: 'default' }),
    // Raw SQL is also supported via { sql, bindValues }
    { sql: 'DELETE FROM todos', bindValues: {} },
  ]),
})
```

## Reading from the database in materializers

Sometimes it can be useful to query your current state when executing a materializer. This can be done by using `ctx.query` in your materializer function.


## `reference/state/materializers/with-query.ts`

```ts filename="reference/state/materializers/with-query.ts"
import { defineMaterializer, Events, Schema, State } from '@livestore/livestore'

import { todos } from './example.ts'

const events = {
  todoCreated: Events.synced({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean.pipe(Schema.optional),
    }),
  }),
} as const

export const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, completed }, ctx) => {
    const previousIds = ctx.query(todos.select('id'))
    // ctx.query also supports raw SQL via { query, bindValues }
    const existingTodos = ctx.query({ query: 'SELECT id FROM todos', bindValues: {} })
    return todos.insert({ id: `${existingTodos.length}-${id}`, text, completed: completed ?? false, previousIds })
  }),
})
```

### `reference/state/materializers/example.ts`

```ts filename="reference/state/materializers/example.ts"
import { defineMaterializer, Events, Schema, State } from '@livestore/livestore'

export const todos = State.SQLite.table({
  name: 'todos',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    text: State.SQLite.text(),
    completed: State.SQLite.boolean({ default: false }),
    previousIds: State.SQLite.json({
      schema: Schema.Array(Schema.String),
      nullable: true,
    }),
  },
})

export const table1 = State.SQLite.table({
  name: 'settings',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    someVal: State.SQLite.integer({ default: 0 }),
  },
})

export const table2 = State.SQLite.table({
  name: 'preferences',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    otherVal: State.SQLite.text({ default: 'default' }),
  },
})

export const events = {
  todoCreated: Events.synced({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean.pipe(Schema.optional),
    }),
  }),
  userPreferencesUpdated: Events.synced({
    name: 'userPreferencesUpdated',
    schema: Schema.Struct({ userId: Schema.String, theme: Schema.String }),
  }),
  factoryResetApplied: Events.synced({
    name: 'factoryResetApplied',
    schema: Schema.Struct({}),
  }),
} as const

export const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, completed }) =>
    todos.insert({ id, text, completed: completed ?? false }),
  ),
  [events.userPreferencesUpdated.name]: defineMaterializer(events.userPreferencesUpdated, ({ userId, theme }) => {
    console.log(`User ${userId} updated theme to ${theme}.`)
    return []
  }),
  [events.factoryResetApplied.name]: defineMaterializer(events.factoryResetApplied, () => [
    table1.update({ someVal: 0 }),
    table2.update({ otherVal: 'default' }),
    // Raw SQL is also supported via { sql, bindValues }
    { sql: 'DELETE FROM todos', bindValues: {} },
  ]),
})
```

## Transactional behaviour

A materializer is always executed in a transaction. This transaction applies to:
- All database write operations returned by the materializer.
- Any `ctx.query` calls made within the materializer, ensuring a consistent view of the data.

Materializers can return:
- A single database write operation.
- An array of database write operations.
- `void` (i.e., no return value) if no database modifications are needed.
- An `Effect` that resolves to one of the above (e.g., `Effect.succeed(writeOp)` or `Effect.void`).

The `context` object passed to each materializer provides `query` for database reads and `event` for the full event details.

## Error handling

If a materializer function throws an error, or if an `Effect` returned by a materializer fails, the entire transaction for that event will be rolled back. This means any database changes attempted by that materializer for the failing event will not be persisted. The error will be logged, and the system will typically halt or flag the event as problematic, depending on the specific LiveStore setup.

If the error happens on the client which tries to commit the event, the event will never be committed and pushed to the sync backend.

In the future there will be ways to configure the error-handling behaviour, e.g. to allow skipping an incoming event when a materializer fails in order to avoid the app getting stuck. However, skipping events might also lead to diverging state across clients and should be used with caution.

## Best practices

### Side-effect free / deterministic

It's strongly recommended to make sure your materializers are side-effect free and deterministic. This also implies passing in all necessary data via the event payload.

Example:


## `reference/state/materializers/deterministic.ts`

```ts filename="reference/state/materializers/deterministic.ts"
import { randomUUID } from 'node:crypto'

import { defineMaterializer, Events, nanoid, Schema, State, type Store } from '@livestore/livestore'

import { todos } from './example.ts'

declare const store: Store

export const nondeterministicEvents = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ text: Schema.String }),
  }),
} as const

export const nondeterministicMaterializers = State.SQLite.materializers(nondeterministicEvents, {
  [nondeterministicEvents.todoCreated.name]: defineMaterializer(nondeterministicEvents.todoCreated, ({ text }) =>
    todos.insert({ id: randomUUID(), text }),
  ),
})

store.commit(nondeterministicEvents.todoCreated({ text: 'Buy groceries' }))

export const deterministicEvents = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
  }),
} as const

export const deterministicMaterializers = State.SQLite.materializers(deterministicEvents, {
  [deterministicEvents.todoCreated.name]: defineMaterializer(deterministicEvents.todoCreated, ({ id, text }) =>
    todos.insert({ id, text }),
  ),
})

store.commit(deterministicEvents.todoCreated({ id: nanoid(), text: 'Buy groceries' }))
```

### `reference/state/materializers/example.ts`

```ts filename="reference/state/materializers/example.ts"
import { defineMaterializer, Events, Schema, State } from '@livestore/livestore'

export const todos = State.SQLite.table({
  name: 'todos',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    text: State.SQLite.text(),
    completed: State.SQLite.boolean({ default: false }),
    previousIds: State.SQLite.json({
      schema: Schema.Array(Schema.String),
      nullable: true,
    }),
  },
})

export const table1 = State.SQLite.table({
  name: 'settings',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    someVal: State.SQLite.integer({ default: 0 }),
  },
})

export const table2 = State.SQLite.table({
  name: 'preferences',
  columns: {
    id: State.SQLite.text({ primaryKey: true }),
    otherVal: State.SQLite.text({ default: 'default' }),
  },
})

export const events = {
  todoCreated: Events.synced({
    name: 'todoCreated',
    schema: Schema.Struct({
      id: Schema.String,
      text: Schema.String,
      completed: Schema.Boolean.pipe(Schema.optional),
    }),
  }),
  userPreferencesUpdated: Events.synced({
    name: 'userPreferencesUpdated',
    schema: Schema.Struct({ userId: Schema.String, theme: Schema.String }),
  }),
  factoryResetApplied: Events.synced({
    name: 'factoryResetApplied',
    schema: Schema.Struct({}),
  }),
} as const

export const materializers = State.SQLite.materializers(events, {
  [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text, completed }) =>
    todos.insert({ id, text, completed: completed ?? false }),
  ),
  [events.userPreferencesUpdated.name]: defineMaterializer(events.userPreferencesUpdated, ({ userId, theme }) => {
    console.log(`User ${userId} updated theme to ${theme}.`)
    return []
  }),
  [events.factoryResetApplied.name]: defineMaterializer(events.factoryResetApplied, () => [
    table1.update({ someVal: 0 }),
    table2.update({ otherVal: 'default' }),
    // Raw SQL is also supported via { sql, bindValues }
    { sql: 'DELETE FROM todos', bindValues: {} },
  ]),
})
```

