Dequel

Dequel (DQL, Data Query Language) is a human-friendly query language that feels familiar to anyone who has used search filters on sites like GitHub, Discord, or Gmail.

# Find posts containing "elixir" in tags
tags:*elixir

# Find high-priority tasks assigned to Sarah
priority:high assignee:sarah !status:completed

# Numeric comparisons and ranges
price:>19.99 quantity:<=100
age:18..65

# Multiple values with OR expansion
category:[frontend, design] status:pending

# Relationship filtering
author.name:Tolkien books { genre:fantasy }

# Many-to-many relationships
tags { name:classic }

Features

See SYNTAX.md for the complete syntax reference.

Installation

Add dequel to your dependencies in mix.exs:

def deps do
  [
    {:dequel, "~> 0.4.0"}
  ]
end

Then run:

mix deps.get

Usage

Basic Filtering with Ecto

import Ecto.Query
alias Dequel.Adapter.Ecto.Filter

# Simple field filtering
from(p in Post)
|> Filter.query("status:published tags:*elixir")
|> Repo.all()

# Relationship paths (auto-joins)
from(p in Post)
|> Filter.query("author.name:Tolkien")
|> Repo.all()

# Block syntax for has_many/belongs_to (requires schema)
from(a in Author)
|> Filter.query("items { rarity:legendary }", schema: Author)
|> Repo.all()

Composing with Ecto Queries

Use Dequel.where/1 to get a dynamic expression you can compose:

import Ecto.Query

user_input = "title:*ring category:[fantasy, adventure]"

from(c in Content)
|> where(^Dequel.where(user_input))
|> where([c], c.status == "published")
|> where([c], c.user_id == ^current_user.id)
|> Repo.all()

Parsing Queries

# Parse a query string into an AST
ast = Dequel.Parser.parse!("status:active name:*frodo")
# => {:and, [], [{:==, [], [:status, "active"]}, {:contains, [], [:name, "frodo"]}]}

Quick Reference

Syntax Meaning
field:value Equality
field:"some value" Equality (quoted)
field:contains(value) Contains
field:*value Contains (shorthand)
field:starts_with(value) Starts with
field:^value Starts with (shorthand)
field:$value Ends with (shorthand)
field:ends_with(value) Ends with
field:one_of(a, b, c) IN
field:[a, b, c] IN (shorthand)
field:>18 Greater than
field:<100 Less than
field:>=0 Greater than or equal
field:<=99 Less than or equal
field:10..50 Between (inclusive range)
field:between(10 50) Between (predicate form)
!field:value NOT
-field:value NOT (alternate)
a:1 b:2 AND (implicit)
a:1 or b:2 OR (keyword)
( expr ) Grouping
rel.field:value Relationship path
rel { field_a:1 field_b:2 } Block syntax (has_many/belongs_to)

Spaces after : are allowed. So this works too:

last_name: Baggins
first_name: contains(o)

Why Dequel?

Dequel provides a query interface that's powerful enough for developers but approachable for end users. It's ideal for:

Demo & Development

The demo/ directory contains a Phoenix LiveView application showcasing Dequel with a live CodeMirror-based editor:

cd demo
mix setup
mix phx.server
# Visit http://localhost:4242

Benchmarking

Run performance benchmarks with realistic datasets:

cd demo
MIX_ENV=bench mix bench              # Small dataset (100 books)
MIX_ENV=bench mix bench --size large # Large dataset (10,000 books)
MIX_ENV=bench mix bench.history      # View historical results

License

MIT