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.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

SyntaxMeaning
field:valueEquality
field:"some value"Equality (quoted)
field:contains(value)Contains
field:*valueContains (shorthand)
field:starts_with(value)Starts with
field:^valueStarts with (shorthand)
field:$valueEnds with (shorthand)
field:ends_with(value)Ends with
field:one_of(a, b, c)IN
field:[a, b, c]IN (shorthand)
field:>18Greater than
field:<100Less than
field:>=0Greater than or equal
field:<=99Less than or equal
field:10..50Between (inclusive range)
field:between(10 50)Between (predicate form)
!field:valueNOT
-field:valueNOT (alternate)
a:1 b:2AND (implicit)
a:1 or b:2OR (keyword)
( expr )Grouping
rel.field:valueRelationship 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