View Source 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

  • Simple field:value filtering that users already know from search interfaces
  • String matching: contains (*), starts_with (^), ends_with ($)
  • Numeric comparisons: >, <, >=, <=, and range syntax (10..50)
  • one_of/IN predicates with bracket shorthand: field:[a, b, c]
  • Negation with ! or - prefix
  • Logical operators (AND/OR) with grouping
  • Relationship filtering via dot notation and block syntax
  • Many-to-many relationship support (e.g., tags, categories)
  • Type coercion with schema support (integers, dates, booleans, etc.)
  • Ecto and ETS adapters included
  • Live demo with CodeMirror-based editor (demo/)

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

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:

  • Adding flexible search/filter interfaces to web applications
  • Building dynamic content collections
  • Creating data-driven features without exposing SQL
  • Providing domain-specific search predicates

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