# Fetch, Update, and Delete APIs

This guide is the high-level map of Lazarus's read, update, and delete surface.

## Read APIs

Lazarus does not add separate read helpers. Instead, it changes the behavior of
ordinary Repo reads by filtering out soft-deleted rows unless you opt in with
`with_deleted: true`.

### Default behavior

Schema-backed reads hide soft-deleted rows:

```elixir
Repo.get(Post, id)
Repo.all(Post)

from(post in Post, select: post.id)
|> Repo.all()
```

### Opting into deleted rows

Pass `with_deleted: true` when you intentionally want deleted rows included:

```elixir
Repo.get(Post, id, with_deleted: true)
Repo.all(Post, with_deleted: true)

from(post in Post, where: post.author_id == ^author_id)
|> Repo.all(with_deleted: true)
```

The same opt works for preloads:

```elixir
post =
  Repo.get!(Post, id, with_deleted: true)
  |> Repo.preload(:comments, with_deleted: true)

account =
  Repo.get!(Account, id)
  |> Repo.preload([posts: [:comments]], with_deleted: true)
```

Joined schema-aware sources follow the same `with_deleted` behavior as query
roots. This means, with `with_deleted: false`, Lazarus will recurse into
supported deeper queries to exclude soft-deleted records. Otherwise, if
`with_deleted: true`, results of the root and joined queries will include
soft-delete records, essentially disabling any Lazarus filters.

Schema-less roots and joins raise by default unless you pass
`allow_schema_less_sources: true`. Raw fragments and direct SQL calls raise by
default unless you pass `allow_raw_sql: true`.

Use
[repo-level `bypass_schemas` / `bypass_tables`](repo-module-setup.md#bypassing-schemas-and-tables)
for third-party sources Lazarus should always leave alone. Repo-level bypasses
apply to configured schema and table sources; when a bypassed source is the
query root, Lazarus leaves that Ecto query alone, including fragments. They do
not allow direct SQL calls.

See [Query Support](query-support.md) for the full query-shape rules.

## Update APIs

For schemas with `soft_deletes()`, Lazarus changes updates so soft-deleted rows
are ignored by default. In practice, already-deleted rows behave like rows that
were physically deleted unless you pass `with_deleted: true`.

### Update one row

For schemas with `soft_deletes()`, `Repo.update/2` and `Repo.update!/2` calls
affect only active rows by default.

```elixir
post =
  Repo.get!(Post, id)
  |> Ecto.Changeset.change(title: "Updated title")

Repo.update(post)
```

If the row was already soft-deleted, Lazarus treats the update as stale. That
means the usual Ecto stale options still apply:

```elixir
Repo.update(changeset, stale_error_field: :deleted_at)
Repo.update(changeset, stale_error_field: :deleted_at, stale_error_message: "was deleted")
Repo.update(changeset, allow_stale: true)
```

Invalid changesets still use Ecto's normal validation error path. Unchanged
non-forced changesets keep Ecto's no-op behavior. `force: true` counts as an
update attempt and does not allow updating a soft-deleted row.

To intentionally update a soft-deleted row, pass `with_deleted: true`:

```elixir
post =
  Repo.get!(Post, id, with_deleted: true)
  |> Ecto.Changeset.change(title: "Administrative correction")

Repo.update(post, with_deleted: true)
```

Schemas without Lazarus soft-delete fields and
[bypassed schemas](repo-module-setup.md#bypassing-schemas-and-tables) keep Ecto
update behavior.

### Insert or update

`Repo.insert_or_update/2` and `Repo.insert_or_update!/2` follow the same rule
when the changeset data is loaded: the update side ignores soft-deleted records
unless `with_deleted: true` is passed.

```elixir
loaded_post
|> Ecto.Changeset.change(title: "Updated title")
|> Repo.insert_or_update()
```

Built changesets still insert normally:

```elixir
%Post{}
|> Ecto.Changeset.change(title: "New post")
|> Repo.insert_or_update()
```

### Update many rows

`Repo.update_all/3` uses Lazarus query filtering. For schema-aware sources,
active rows are updated by default and soft-deleted rows are skipped.

```elixir
from(post in Post, where: post.author_id == ^author_id)
|> Repo.update_all(set: [status: "archived"])
```

Pass `with_deleted: true` when a bulk update should include soft-deleted rows:

```elixir
from(post in Post, where: post.author_id == ^author_id)
|> Repo.update_all([set: [status: "archived"]], with_deleted: true)
```

Lazarus follows Ecto's bulk-update return shape: `{count, nil}` without a
`select`, and `{count, returned}` when a `select` is present. The `count` and
returned rows reflect what was actually updated.

Joined schema-aware sources, subqueries, CTEs, and update expressions follow the
same query-shape rules as reads. Prefer schema-aware sources for bulk updates.
Schema-less update targets raise by default; `allow_schema_less_sources: true`
is an explicit escape hatch, and those updates run unfiltered because Lazarus
has no schema metadata to identify soft-deleted rows. Raw SQL fragments require
`allow_raw_sql: true`.

See [Query Support](query-support.md) for the exact update query rules.

## Delete APIs

### Soft-delete one row

Use `Repo.soft_delete/2` when you want to keep the row and mark it deleted.

```elixir
{:ok, deleted_post} = Repo.soft_delete(post, reason: "Deleted by user")
```

Pass `cascade: true` when eligible associations should also be deleted:

```elixir
{:ok, deleted_post} = Repo.soft_delete(post, cascade: true)
```

See [Cascade Soft-Deletes](cascade-soft-deletes.md) for more details.

Use `Repo.soft_delete!/2` when you want the same behavior but prefer a raising
API.

```elixir
deleted_post = Repo.soft_delete!(post)
deleted_post = Repo.soft_delete!(post, reason: "Moderator action")
deleted_post = Repo.soft_delete!(post, reason: "Moderator action", reload_after_delete: true)
```

Under the bonnet, all soft deletes (single row `soft_delete` and bulk
`soft_delete_all`) use the `update_all` function. Single-row `soft_delete`
builds a primary-key query, runs it through the same bulk path, and returns
`{:error, :not_found}` when no active row was updated because the row was
already soft-deleted or no longer exists.

Because of that, we do not automatically get the most up-to-date row data as a
return value, like we would from a single-row operation such as `update` or
`delete`.

By default, single-row soft delete (`Repo.soft_delete/2`) returns an in-memory
copy of the loaded struct with soft-delete fields applied and loaded
associations reset to `Ecto.Association.NotLoaded`. That avoids a follow-up
read, keeping soft-deletes cheap and soft-delete fields up-to-date.

If you absolutely need fresh data, you can pass `reload_after_delete: true`,
which will perform an additional `Repo.get!/3` with `with_deleted: true` after
the update succeeds. This keeps the data returned fresh, at the cost of an
additional database call.

```elixir
{:ok, deleted_post} = Repo.soft_delete(post, reload_after_delete: true)
```

For the exact return values and option list, see `Lazarus.soft_delete/3`,
`Lazarus.soft_delete!/3`, and the injected Repo docs described in
`Lazarus.Repo`.

### Soft-delete many rows

Use `Repo.soft_delete_all/2` when you want to mark every row in a schema-aware
query as deleted.

```elixir
{2, nil} =
  Repo.soft_delete_all(
    from(post in Post, where: post.author_id == ^author_id),
    reason: "Bulk cleanup"
  )

{2, titles} =
  Repo.soft_delete_all(
    from(post in Post,
      where: post.author_id == ^author_id,
      select: post.title
    )
  )
```

Lazarus follows Ecto's bulk-update return shape here: `{count, nil}` without a
`select`, and `{count, returned}` when a `select` is present.

`Repo.soft_delete_all/2` requires a schema-aware root query. See
[Query Support](query-support.md) for the allowed query shapes.

Pass `cascade: true` when the bulk soft delete should also traverse eligible
associations:

```elixir
Repo.soft_delete_all(query, cascade: true)
```

See [Cascade Soft-Deletes](cascade-soft-deletes.md) for more details.

### Hard-delete one row

Use `Repo.hard_delete/2` when you explicitly want ordinary physical delete
semantics.

```elixir
{:ok, _post} = Repo.hard_delete(post)
deleted_post = Repo.hard_delete!(post)
```

Use `Repo.hard_delete!/2` when you prefer the raising variant.

### Hard-delete many rows

Use `Repo.hard_delete_all/2` for explicit bulk physical deletes.

```elixir
{3, nil} =
  Repo.hard_delete_all(
    from(row in "audit_logs", where: field(row, :inserted_at) < ^cutoff)
  )

{3, ids} =
  Repo.hard_delete_all(
    from(post in Post,
      where: post.author_id == ^author_id,
      select: post.id
    )
  )
```

Like ordinary Ecto bulk deletes, the return value is `{count, nil}` without a
`select`, and `{count, returned}` when a `select` is present.

Hard delete is the escape hatch, so it remains available for schema-aware
queries and schema-less table queries that Ecto can delete from.

### Ordinary `Repo.delete*` calls

Outside bypassed schemas and tables, Lazarus disables direct `Repo.delete*` and
`Repo.delete_all*` calls:

- `Repo.delete/1`
- `Repo.delete/2`
- `Repo.delete!/1`
- `Repo.delete!/2`
- `Repo.delete_all/1`
- `Repo.delete_all/2`

Use `Repo.soft_delete*` or `Repo.hard_delete*` instead.

## Repo APIs vs `Lazarus` Helpers

The `Repo.*` functions are the usual application-facing API:

- ordinary Ecto read functions such as `Repo.get/3`, `Repo.all/2`, and
  `Repo.one/2`
- Ecto update functions with Lazarus soft-delete behavior, such as
  `Repo.update/2`, `Repo.update!/2`, `Repo.update_all/3`,
  `Repo.insert_or_update/2`, and `Repo.insert_or_update!/2`
- `Repo.soft_delete/2`
- `Repo.soft_delete!/2`
- `Repo.soft_delete_all/2`
- `Repo.hard_delete/2`
- `Repo.hard_delete!/2`
- `Repo.hard_delete_all/2`

The `Lazarus.*` helpers are the repo-explicit equivalents for cases where you
want to pass the Repo module yourself:

- `Lazarus.soft_delete(Repo, struct_or_changeset, opts)`
- `Lazarus.soft_delete!(Repo, struct_or_changeset, opts)`
- `Lazarus.soft_delete_all(Repo, queryable, opts)`

For most application code, the Repo API is the natural choice. The explicit
`Lazarus.*` helpers are more useful in shared helpers, tests, or integrations
that should not assume a specific Repo module has already imported the injected
functions. There are no repo-explicit Lazarus helpers for updates; use the Repo
update functions listed above, with the soft-delete behavior added by
`use Lazarus`.
