README.md

# ExSift

MongoDB-style query filtering for Elixir collections.

ExSift is an Elixir library inspired by [sift.js](https://github.com/crcn/sift.js) that brings MongoDB's powerful query syntax to Elixir. Filter lists, maps, and any enumerable with an expressive, familiar query language.

## Features

- **MongoDB-compatible query syntax** - Use the same operators you know from MongoDB
- **Type-safe** - Full Elixir typespecs and pattern matching
- **Comprehensive operators** - Support for comparison, logical, array, and special operators
- **Nested property access** - Query deeply nested maps with dot notation
- **Regex support** - Pattern matching with Elixir's `Regex` module
- **Date/Time support** - Compare `DateTime`, `NaiveDateTime`, and `Date` types
- **Well-tested** - 40+ tests covering all operators and edge cases

## Installation

Add `ex_sift` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ex_sift, "~> 0.1.0"}
  ]
end
```

## Quick Start

```elixir
data = [
  %{name: "Alice", age: 30, city: "NYC"},
  %{name: "Bob", age: 25, city: "SF"},
  %{name: "Charlie", age: 35, city: "NYC"}
]

# Simple equality
ExSift.filter(data, %{city: "NYC"})
# => [%{name: "Alice", ...}, %{name: "Charlie", ...}]

# Comparison operators
ExSift.filter(data, %{age: %{"$gt" => 28}})
# => [%{name: "Alice", age: 30, ...}, %{name: "Charlie", age: 35, ...}]

# Multiple conditions
ExSift.filter(data, %{city: "NYC", age: %{"$gte" => 30}})
# => [%{name: "Alice", ...}, %{name: "Charlie", ...}]
```

## Supported Operators

### Comparison Operators

- **`$eq`** - Equals (same as direct value)
  ```elixir
  ExSift.filter(data, %{age: %{"$eq" => 30}})
  ```

- **`$ne`** - Not equals
  ```elixir
  ExSift.filter(data, %{city: %{"$ne" => "NYC"}})
  ```

- **`$gt`** - Greater than
  ```elixir
  ExSift.filter(data, %{age: %{"$gt" => 25}})
  ```

- **`$gte`** - Greater than or equal
  ```elixir
  ExSift.filter(data, %{age: %{"$gte" => 30}})
  ```

- **`$lt`** - Less than
  ```elixir
  ExSift.filter(data, %{age: %{"$lt" => 30}})
  ```

- **`$lte`** - Less than or equal
  ```elixir
  ExSift.filter(data, %{age: %{"$lte" => 25}})
  ```

### Logical Operators

- **`$and`** - All conditions must match
  ```elixir
  ExSift.filter(data, %{
    "$and" => [
      %{age: %{"$gte" => 25}},
      %{city: "NYC"}
    ]
  })
  ```

- **`$or`** - At least one condition must match
  ```elixir
  ExSift.filter(data, %{
    "$or" => [
      %{age: %{"$lt" => 26}},
      %{city: "LA"}
    ]
  })
  ```

- **`$nor`** - No conditions match
  ```elixir
  ExSift.filter(data, %{
    "$nor" => [
      %{city: "NYC"},
      %{city: "SF"}
    ]
  })
  ```

- **`$not`** - Negation
  ```elixir
  ExSift.filter(data, %{age: %{"$not" => %{"$lt" => 30}}})
  ```

### Array Operators

- **`$in`** - Value in array
  ```elixir
  ExSift.filter(data, %{city: %{"$in" => ["NYC", "LA", "SF"]}})
  ```

- **`$nin`** - Value not in array
  ```elixir
  ExSift.filter(data, %{city: %{"$nin" => ["NYC"]}})
  ```

- **`$all`** - Array contains all values
  ```elixir
  ExSift.filter(data, %{tags: %{"$all" => ["admin", "user"]}})
  ```

- **`$elemMatch`** - Array element matches query
  ```elixir
  data = [%{items: [%{id: 1, active: true}]}]
  ExSift.filter(data, %{items: %{"$elemMatch" => %{id: 1, active: true}}})
  ```

- **`$size`** - Array has specific length
  ```elixir
  ExSift.filter(data, %{tags: %{"$size" => 3}})
  ```

### Other Operators

- **`$exists`** - Field exists (not nil)
  ```elixir
  ExSift.filter(data, %{email: %{"$exists" => true}})
  ExSift.filter(data, %{phone: %{"$exists" => false}})
  ```

- **`$type`** - Type checking
  ```elixir
  ExSift.filter(data, %{age: %{"$type" => "number"}})
  ExSift.filter(data, %{name: %{"$type" => "string"}})
  # Supported types: "string", "number", "integer", "float",
  #                  "boolean", "map", "list", "atom", "date", "datetime", "nil"
  ```

- **`$mod`** - Modulus operation
  ```elixir
  ExSift.filter(data, %{count: %{"$mod" => [5, 0]}})  # divisible by 5
  ```

- **`$regex`** - Regular expression matching
  ```elixir
  # Using Elixir regex
  ExSift.filter(data, %{name: ~r/^[AB]/})

  # Using $regex operator
  ExSift.filter(data, %{email: %{"$regex" => "@gmail\\.com$"}})
  ```

## Advanced Usage

### Nested Properties

Use dot notation to query nested maps:

```elixir
data = [
  %{user: %{profile: %{age: 30, verified: true}}},
  %{user: %{profile: %{age: 25, verified: false}}}
]

ExSift.filter(data, %{"user.profile.age" => %{"$gt" => 28}})
# => [%{user: %{profile: %{age: 30, ...}}}]
```

### Complex Queries

Combine multiple operators for powerful filtering:

```elixir
data = [
  %{name: "Alice", age: 30, status: "active", tags: ["admin"]},
  %{name: "Bob", age: 25, status: "inactive", tags: ["user"]},
  %{name: "Charlie", age: 35, status: "active", tags: ["admin", "moderator"]}
]

ExSift.filter(data, %{
  "$and" => [
    %{age: %{"$gte" => 25, "$lte" => 35}},
    %{status: "active"},
    %{tags: %{"$in" => ["admin"]}}
  ]
})
# => [%{name: "Alice", ...}, %{name: "Charlie", ...}]
```

### Utility Functions

ExSift provides several utility functions beyond `filter/2`:

```elixir
data = [%{a: 1}, %{a: 2}, %{a: 3}]

# Test a single item
ExSift.test(%{a: 1}, %{a: 1})  # => true

# Find first matching item
ExSift.find(data, %{a: 2})  # => %{a: 2}

# Check if any match
ExSift.any?(data, %{a: %{"$gt" => 2}})  # => true

# Check if all match
ExSift.all?(data, %{a: %{"$gte" => 1}})  # => true

# Count matches
ExSift.count(data, %{a: %{"$lt" => 3}})  # => 2

# Compile query for reuse
tester = ExSift.compile(%{a: %{"$gt" => 1}})
tester.(%{a: 2})  # => true
tester.(%{a: 1})  # => false
```

## Architecture

ExSift is built with three main modules:

- **`ExSift`** - Main API and utility functions
- **`ExSift.Query`** - Query parsing and matching logic
- **`ExSift.Operators`** - Operator implementations

The library leverages Elixir's pattern matching and protocol system for extensible, type-safe query operations.

### Comparison with sift.js

ExSift is inspired by [sift.js](https://github.com/crcn/sift.js) but adapted for Elixir's functional programming paradigm:

| Feature | sift.js | ExSift |
|---------|---------|--------|
| Language | JavaScript/TypeScript | Elixir |
| Architecture | Operation classes with state | Pattern matching + pure functions |
| Extensibility | Custom operations via options | Protocol-based (future) |
| Type Safety | TypeScript generics | Dialyzer typespecs |
| Immutability | Depends on usage | Built-in (Elixir default) |

## Performance

ExSift uses single-pass filtering with early termination where possible. All operations are implemented as pure functions without side effects.

For large datasets, consider:
- Using `ExSift.compile/1` to create reusable query functions
- Leveraging `ExSift.find/2` or `ExSift.any?/2` for early termination
- Pre-filtering with simpler queries before complex ones

## Testing

Run the test suite:

```bash
mix test
```

ExSift includes 40+ tests covering:
- All operators
- Nested property access
- Complex query combinations
- Edge cases and error handling

## License

MIT License - See LICENSE file for details

## Acknowledgments

Inspired by [sift.js](https://github.com/crcn/sift.js) by Craig Condon.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.