> ## Documentation Index
> Fetch the complete documentation index at: https://cindel.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Querying Cindel Collections with the Typed Query API

> Use all(), where(), and filter() to read, count, sort, paginate, update, and delete documents from Cindel collections with type-safe query builders.

Every Cindel query starts from a generated collection, builds up conditions and result options, then executes with a result method. This page walks through the three entry points — `all()`, `where()`, and `filter()` — along with sorting, pagination, distinct, dynamic modifiers, query deletes, and query updates.

## Query Flow

<Steps>
  <Step title="Choose an entry point">
    Start with `all()` for the full collection, `where()` for an indexed-field lookup, or `filter()` for a predicate over any persisted field.
  </Step>

  <Step title="Chain modifiers">
    Add filters, sort helpers, `offset`, `limit`, or `distinct` calls as needed.
  </Step>

  <Step title="Execute">
    Call `findAll()`, `findFirst()`, or `count()` to run the query and receive results.
  </Step>
</Steps>

A minimal example with the `Todo` model below illustrates the full shape:

```dart theme={null}
@Collection(name: 'todos')
class Todo {
  Id dbId = autoIncrement;

  @Index()
  late String title;

  bool completed = false;

  @Index()
  DateTime createdAt = DateTime.now().toUtc();

  @Index(type: CindelIndexType.multiEntry, caseSensitive: false)
  List<String> tags = const [];
}
```

```dart theme={null}
final openTodos = await db.todos
    .filter()
    .completedEqualTo(false)
    .findAll();
```

## Entry Points

### `all()` — Full Collection

Use `all()` when the query should consider every object in the collection. It is the right starting point for full lists, counts, sorted pages, and maintenance operations.

```dart theme={null}
// Fetch every todo
final todos = await db.todos.all().findAll();

// Count them all
final total = await db.todos.all().count();

// Newest 20
final newest = await db.todos
    .all()
    .sortByCreatedAt(order: CindelSortOrder.descending)
    .limit(20)
    .findAll();
```

For large collections, always add sorting and pagination rather than loading everything at once.

### `where()` — Indexed-Field Lookups

Use `where()` for equality, range, and multi-entry lookups. **The field must be annotated with `@Index` in your model** — `where()` helpers are only generated for indexed fields, and using a non-indexed field as a `where()` entry point is not supported. Generated helpers are named after your model fields and indexes.

```dart theme={null}
// Exact match on an indexed string field
final todo = await db.todos
    .where()
    .titleEqualTo('Ship docs')
    .findFirst();

// Range query on an indexed DateTime field
final createdThisWeek = await db.todos
    .where()
    .createdAtBetween(startOfWeek, endOfWeek)
    .findAll();

// Multi-entry index — list membership
final urgent = await db.todos
    .where()
    .tagsContains('urgent')
    .findAll();
```

<Tip>
  Use `where()` for fields you query frequently — indexed lookups are far cheaper than full-collection scans. If a helper you expect does not exist, verify that the field is annotated with `@Index` and that generated code is up to date.
</Tip>

### `filter()` — Predicate-Based Filtering

Use `filter()` to apply predicates over any persisted field, whether or not it is indexed. You can also chain `filter()` after `where()` to narrow an already-indexed candidate set.

```dart theme={null}
// Filter on its own
final done = await db.todos
    .filter()
    .completedEqualTo(true)
    .findAll();

// filter() after where() for best performance
final urgentOpen = await db.todos
    .where()
    .tagsContains('urgent')
    .filter()
    .completedEqualTo(false)
    .findAll();
```

<Tip>
  Combining `where()` and `filter()` gives you the best of both worlds: the index narrows the candidate set cheaply, and the filter applies the remaining condition only to that smaller set.
</Tip>

## Result Methods

Result methods execute the query. Until you call one, you are building — not running — the query.

| Method        | Returns   | Use when…                          |
| ------------- | --------- | ---------------------------------- |
| `findAll()`   | `List<T>` | You need every matching object     |
| `findFirst()` | `T?`      | You need one object or null        |
| `count()`     | `int`     | You need a number, not the objects |

```dart theme={null}
// All matching objects
final todos = await db.todos
    .filter()
    .completedEqualTo(false)
    .findAll();

// First match or null
final todo = await db.todos
    .where()
    .titleEqualTo('Ship docs')
    .findFirst();

if (todo == null) {
  // No matching todo exists.
}

// Just the count
final openCount = await db.todos
    .filter()
    .completedEqualTo(false)
    .count();

final hasOpenWork = openCount > 0;
```

## Sorting

Generated sort helpers are available for every persisted field. Use `sortBy…` for the primary sort and chain `thenBy…` for secondary sorts. Pass `order: CindelSortOrder.descending` to reverse any sort.

```dart theme={null}
final newest = await db.todos
    .all()
    .sortByCreatedAt(order: CindelSortOrder.descending)
    .thenByTitle()
    .findAll();
```

When you need to sort by a field name that is only known at runtime, use the lower-level string API:

```dart theme={null}
final sorted = await db.todos
    .all()
    .sortBy('createdAt', order: CindelSortOrder.descending)
    .thenBy('title')
    .findAll();
```

Prefer generated sort helpers whenever the field is known at compile time — the string API is for dynamic code that already works with persisted field names.

## Pagination

Use `offset` and `limit` to page through results. Both values must be non-negative.

* `offset(n)` — skip the first `n` results after filtering, sorting, and distinct.
* `limit(n)` — return at most `n` results after the offset.

```dart theme={null}
Future<List<Todo>> loadTodoPage({
  required int page,
  required int pageSize,
}) {
  return db.todos
      .all()
      .sortByCreatedAt(order: CindelSortOrder.descending)
      .offset(page * pageSize)
      .limit(pageSize)
      .findAll();
}
```

## Distinct

Use `distinct` to keep only the first result for each unique value of a field, or for each unique combination of multiple fields. Distinct is applied before `offset` and `limit`.

```dart theme={null}
// Deduplicate by a single field
final distinctTitles = await db.todos
    .all()
    .distinctByTitle()
    .findAll();

// Deduplicate by a combination of fields
final distinctPairs = await db.todos
    .all()
    .distinctByFields(['completed', 'title'])
    .findAll();
```

When you care which record is kept for each distinct value, sort first:

```dart theme={null}
// Keep the most-recently-created todo per title
final latestByTitle = await db.todos
    .all()
    .sortByCreatedAt(order: CindelSortOrder.descending)
    .distinctByTitle()
    .findAll();
```

## Dynamic Query Modifiers

Dynamic modifiers let you build queries from optional user input without duplicating query logic across multiple branches.

### `optional` — Conditional Filter

Apply a filter only when a condition is true. When the condition is `false`, the query passes through unchanged.

```dart theme={null}
Future<List<Todo>> searchTodos(String search) {
  final text = search.trim();

  return db.todos
      .filter()
      .optional(text.isNotEmpty, (q) => q.titleContains(text))
      .findAll();
}
```

### `anyOf` — OR Across a List

Match records that satisfy at least one condition from a list. An empty list matches nothing.

```dart theme={null}
final selectedTags = ['docs', 'urgent'];

final todos = await db.todos
    .filter()
    .anyOf(selectedTags, (q, tag) {
      return q.tagsElementEqualTo(tag);
    })
    .findAll();
```

### `allOf` — AND Across a List

Match records that satisfy every condition from a list. An empty list is a no-op.

```dart theme={null}
final requiredWords = ['api', 'docs'];

final todos = await db.todos
    .filter()
    .allOf(requiredWords, (q, word) {
      return q.titleContains(word);
    })
    .findAll();
```

<Note>
  The callback passed to `anyOf` or `allOf` should only add filters. Do not use it to change sorting, distinct, pagination, projection, or the query source.
</Note>

## Query Deletes

Use query deletes to remove objects by condition rather than by known ids. Both delete methods must run inside a `writeTxn`.

```dart theme={null}
// Delete the first matching object — returns true if one was deleted
final deletedOne = await db.writeTxn(() =>
    db.todos
        .filter()
        .completedEqualTo(true)
        .deleteFirst(),
);

// Delete every matching object — returns the count of deleted objects
final deletedCount = await db.writeTxn(() =>
    db.todos
        .filter()
        .completedEqualTo(true)
        .deleteAll(),
);
```

You can combine `where()` and `filter()` for targeted cleanup:

```dart theme={null}
final removed = await db.writeTxn(() =>
    db.todos
        .where()
        .createdAtLessThan(cutoff)
        .filter()
        .completedEqualTo(true)
        .deleteAll(),
);
```

Use collection `delete` or `deleteAll` when you already have ids. Use query deletes when the removal is driven by a query condition.

## Query Updates

Use query updates to mutate matching objects by persisted field name. Both update methods must run inside a `writeTxn`.

```dart theme={null}
// Update the first matching object — returns true if one was updated
final updatedOne = await db.writeTxn(() =>
    db.todos
        .where()
        .titleEqualTo('Ship docs')
        .updateFirst({'completed': true}),
);

// Update every matching object — returns the count of updated objects
final updatedCount = await db.writeTxn(() =>
    db.todos
        .filter()
        .completedEqualTo(false)
        .updateAll({'completed': true}),
);
```

The map keys are persisted field names. The id field cannot be updated. Values must use Cindel-compatible stored shapes: `null`, `bool`, `int`, finite `double`, `String`, lists, and string-keyed maps.

<Note>
  Query updates are not supported while sync is enabled. When sync is active, read the objects, mutate them in Dart, and write them back with `put` or `putAll`:

  ```dart theme={null}
  final todos = await db.todos
      .filter()
      .completedEqualTo(false)
      .findAll();

  for (final todo in todos) {
    todo.completed = true;
  }

  await db.todos.putAll(todos);
  ```
</Note>

For converted fields — `DateTime`, `Duration`, or enums — prefer typed object writes unless you intentionally want to supply the stored scalar representation. Generated helpers apply the correct conversions automatically; the update map does not.

## Examples

<CodeGroup>
  ```dart Find by indexed field theme={null}
  final todo = await db.todos
      .where()
      .titleEqualTo('Ship docs')
      .findFirst();
  ```

  ```dart Filter open todos theme={null}
  final openTodos = await db.todos
      .filter()
      .completedEqualTo(false)
      .findAll();
  ```

  ```dart Indexed tag + filter theme={null}
  final urgentOpen = await db.todos
      .where()
      .tagsContains('urgent')
      .filter()
      .completedEqualTo(false)
      .findAll();
  ```

  ```dart Paginate newest results theme={null}
  final newest = await db.todos
      .all()
      .sortByCreatedAt(order: CindelSortOrder.descending)
      .limit(20)
      .findAll();
  ```

  ```dart Conditional search theme={null}
  final text = search.trim();

  final results = await db.todos
      .filter()
      .optional(text.isNotEmpty, (q) => q.titleContains(text))
      .findAll();
  ```

  ```dart Count completed theme={null}
  final completedCount = await db.todos
      .filter()
      .completedEqualTo(true)
      .count();
  ```

  ```dart Mark one item updated theme={null}
  final updated = await db.writeTxn(() =>
      db.todos
          .where()
          .titleEqualTo('Ship docs')
          .updateFirst({'completed': true}),
  );
  ```

  ```dart Delete by condition theme={null}
  final deleted = await db.writeTxn(() =>
      db.todos
          .filter()
          .completedEqualTo(true)
          .deleteAll(),
  );
  ```
</CodeGroup>
