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

# Filtering Cindel Queries: Type-Safe and Dynamic APIs

> Use generated type-safe filter helpers or CindelFilter for dynamic runtime predicates, embedded object paths, and logical composition in Cindel queries.

Filters describe conditions that stored objects must satisfy. Cindel gives you two complementary approaches: generated type-safe helpers that follow your Dart model fields, and a lower-level `CindelFilter` builder for constructing predicates dynamically at runtime. Use generated helpers for everyday application code; reach for `CindelFilter` when filter logic must be assembled from runtime data.

## Generated Filters

Generated filters start from a typed collection's `.filter()` builder. Every helper is derived from a persisted model field — named after it, typed to it, and aware of any custom conversions applied to it.

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

  late String title;
  bool completed = false;
  List<String> tags = const [];
}
```

Given that model, the generated API exposes field-level predicates directly:

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

// String containment
final docs = await db.todos
    .filter()
    .titleContains('docs')
    .findAll();
```

Chain multiple conditions to combine them with AND semantics:

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

Generated helpers are compile-time checked, follow Dart naming conventions, and automatically apply any conversions defined in your model. They are the right choice whenever the field and predicate are known at compile time.

## `CindelFilter` — Dynamic Filters

`CindelFilter` builds predicates from persisted field names, making it suitable for scenarios where the field or condition is only known at runtime. Apply a dynamic predicate to a query with `whereMatches(...)`.

```dart theme={null}
final predicate = CindelFilter.field('completed').equalTo(false);

final open = await db.todos
    .all()
    .whereMatches(predicate)
    .findAll();
```

This is especially useful for saved search definitions, configurable admin screens, and filter builders driven by user input:

```dart theme={null}
CindelFilter buildTodoPredicate(String field, Object? value) {
  return CindelFilter.field(field).equalTo(value);
}
```

<Warning>
  When using `CindelFilter` with converted types — enums, `DateTime`, or `Duration` — you must supply the stored scalar representation yourself. Generated filter helpers apply the correct conversions automatically, so prefer them for these types. Using raw `CindelFilter` predicates with converted fields can silently produce incorrect results.
</Warning>

When a model uses `@Name`, the persisted field name may differ from the Dart field name. Pass the persisted name to `CindelFilter.field(...)`. Generated helpers avoid this issue entirely.

## Field Predicates

The following predicates are available on `CindelFilter.field(...)` (and have generated equivalents for each persisted model field):

| Predicate              | Description                  |
| ---------------------- | ---------------------------- |
| `equalTo`              | Exact equality               |
| `greaterThan`          | Strictly greater than        |
| `greaterThanOrEqualTo` | Greater than or equal to     |
| `lessThan`             | Strictly less than           |
| `lessThanOrEqualTo`    | Less than or equal to        |
| `between`              | Inclusive range              |
| `contains`             | String / list containment    |
| `startsWith`           | String prefix match          |
| `endsWith`             | String suffix match          |
| `isEmpty`              | Empty string or empty list   |
| `isNotEmpty`           | Non-empty string or list     |
| `lengthEqualTo`        | Exact length                 |
| `lengthLessThan`       | Length strictly less than    |
| `lengthGreaterThan`    | Length strictly greater than |
| `lengthBetween`        | Length in inclusive range    |

Examples using `CindelFilter`:

```dart theme={null}
// Numeric comparison
final highPriority = await db.tasks
    .all()
    .whereMatches(CindelFilter.field('priority').greaterThan(5))
    .findAll();

// String containment
final titledDocs = await db.todos
    .all()
    .whereMatches(CindelFilter.field('title').contains('docs'))
    .findAll();

// List non-empty
final tagged = await db.todos
    .all()
    .whereMatches(CindelFilter.field('tags').isNotEmpty())
    .findAll();
```

When the field is known at compile time, use the generated equivalent instead:

```dart theme={null}
final titledDocs = await db.todos
    .filter()
    .titleContains('docs')
    .findAll();
```

## Embedded Object Paths

### Generated Embedded Filters

When data is stored inside embedded objects, the generated API exposes nested filter callbacks that traverse the embedded structure in a type-safe way:

```dart theme={null}
final messages = await db.emails
    .filter()
    .sender((sender) {
      return sender.addressEqualTo('ada@example.com');
    })
    .findAll();
```

For list-typed embedded fields, use the generated element helper:

```dart theme={null}
final sentToMary = await db.emails
    .filter()
    .recipientsElement((recipient) {
      return recipient.addressEqualTo('mary@example.com');
    })
    .findAll();
```

### Dynamic Paths with `CindelFilter.path`

Use `CindelFilter.path([...])` when an embedded field path must be assembled at runtime. Provide the path as a list of persisted field name segments.

```dart theme={null}
final matches = await db.emails
    .all()
    .whereMatches(
      CindelFilter.path(['sender', 'address']).equalTo('ada@example.com'),
    )
    .findAll();
```

When the path reaches a list, Cindel evaluates the remaining path against each element and matches the document if any element satisfies the predicate:

```dart theme={null}
final matches = await db.emails
    .all()
    .whereMatches(
      CindelFilter.path(['recipients', 'address']).equalTo('mary@example.com'),
    )
    .findAll();
```

Use `CindelFilter.path(...)` only when the path cannot be determined at compile time. For known paths, the generated nested filter callbacks are safer and more readable.

## Boolean Composition

Compose multiple `CindelFilter` predicates with `all`, `any`, and `not`.

### `CindelFilter.all` — AND

Every predicate in the list must match:

```dart theme={null}
final predicate = CindelFilter.all([
  CindelFilter.field('completed').equalTo(false),
  CindelFilter.field('title').contains('docs'),
]);

final matches = await db.todos
    .all()
    .whereMatches(predicate)
    .findAll();
```

### `CindelFilter.any` — OR

At least one predicate in the list must match:

```dart theme={null}
final predicate = CindelFilter.any([
  CindelFilter.field('title').contains('release'),
  CindelFilter.field('title').contains('docs'),
]);

final matches = await db.todos
    .all()
    .whereMatches(predicate)
    .findAll();
```

### `CindelFilter.not` — Negation

Invert a predicate:

```dart theme={null}
final notDone = CindelFilter.not(
  CindelFilter.field('completed').equalTo(true),
);

final open = await db.todos
    .all()
    .whereMatches(notDone)
    .findAll();
```

### Composition with Generated Helpers

When the field helpers are known at compile time, the `anyOf` and `allOf` query modifiers are often more readable than manual `CindelFilter` composition:

```dart theme={null}
final matches = await db.todos
    .filter()
    .anyOf(words, (q, word) => q.titleContains(word))
    .findAll();
```

Use `CindelFilter` boolean composition when you need to build a predicate tree directly — for example, when the structure itself is determined at runtime.

## Choosing the Right Approach

<Tabs>
  <Tab title="Generated filters (preferred)">
    Use generated filters for all normal application code. They are compile-time checked, follow your Dart model names, and apply conversions automatically.

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

    Combine `where()` for indexed lookups with `filter()` for additional conditions:

    ```dart theme={null}
    final urgentOpen = await db.todos
        .where()
        .tagsContains('urgent')
        .filter()
        .completedEqualTo(false)
        .findAll();
    ```
  </Tab>

  <Tab title="CindelFilter (dynamic)">
    Use `CindelFilter` when the field name or predicate is only known at runtime:

    * Saved search definitions
    * Configurable admin screens
    * User-selected fields
    * Advanced tooling
    * Filter builders not known at compile time

    ```dart theme={null}
    final predicate = CindelFilter.all([
      CindelFilter.field('completed').equalTo(false),
      CindelFilter.field('title').contains('docs'),
    ]);

    final matches = await db.todos
        .all()
        .whereMatches(predicate)
        .findAll();
    ```
  </Tab>
</Tabs>

<Tip>
  Avoid using filters as a substitute for indexes when lookup performance matters. Add `@Index` to fields that frequently narrow large collections, start the query with `where()`, and add filters only for the remaining conditions.
</Tip>
