EdgeQL
EdgeQL is the query language of EdgeDB. It’s intended as a spiritual successor to SQL that solves some of its biggest design limitations. This page is intended as a rapid-fire overview so you can hit the ground running with EdgeDB. Refer to the linked pages for more in-depth documentation.
Want to follow along with the queries below? Open the Interactive Tutorial in a separate tab. Copy and paste the queries below and execute them directly from the browser.
The examples below also demonstrate how to express the query with the TypeScript client’s query builder, which lets you express arbitrary EdgeQL queries in a code-first, typesafe way.
Scalar literals
EdgeDB has a rich primitive type system consisting of the following data types.
Strings |
|
Booleans |
|
Numbers |
|
UUID |
|
JSON |
|
Dates and times |
|
Durations |
|
Binary data |
|
Auto-incrementing counters |
|
Enums |
|
Basic literals can be declared using familiar syntax.
db>
select "i ❤️ edgedb"; # str
{'i ❤️ edgedb'}
db>
select false; # bool
{false}
db>
select 42; # int64
{42}
db>
select 3.14; # float64
{3.14}
db>
select 12345678n; # bigint
{12345678n}
db>
select 15.0e+100n; # decimal
{15.0e+100n}
db>
select b'bina\\x01ry'; # bytes
{b'bina\\x01ry'}
e.str("i ❤️ edgedb")
// string
e.bool(false)
// boolean
e.int64(42)
// number
e.float64(3.14)
// number
e.bigint(BigInt(12345678))
// bigint
e.decimal("1234.4567")
// n/a (not supported by JS clients)
e.bytes(Buffer.from("bina\\x01ry"))
// Buffer
Other type literals are declared by casting an appropriately structured string.
db>
select <uuid>'a5ea6360-75bd-4c20-b69c-8f317b0d2857';
{a5ea6360-75bd-4c20-b69c-8f317b0d2857}
db>
select <datetime>'1999-03-31T15:17:00Z';
{<datetime>'1999-03-31T15:17:00Z'}
db>
select <duration>'5 hours 4 minutes 3 seconds';
{<duration>'5:04:03'}
db>
select <cal::relative_duration>'2 years 18 days';
{<cal::relative_duration>'P2Y18D'}
e.uuid("a5ea6360-75bd-4c20-b69c-8f317b0d2857")
// string
e.datetime("1999-03-31T15:17:00Z")
// Date
e.duration("5 hours 4 minutes 3 seconds")
// edgedb.Duration (custom class)
e.cal.relative_duration("2 years 18 days")
// edgedb.RelativeDuration (custom class)
Primitive data can be composed into arrays and tuples, which can themselves be nested.
db>
select ['hello', 'world'];
{['hello', 'world']}
db>
select ('Apple', 7, true);
{('Apple', 7, true)} # unnamed tuple
db>
select (fruit := 'Apple', quantity := 3.14, fresh := true);
{(fruit := 'Apple', quantity := 3.14, fresh := true)} # named tuple
db>
select <json>["this", "is", "an", "array"];
{"[\"this\", \"is\", \"an\", \"array\"]"}
e.array(["hello", "world"]);
// string[]
e.tuple(["Apple", 7, true]);
// [string, number, boolean]
e.tuple({fruit: "Apple", quantity: 3.14, fresh: true});
// {fruit: string; quantity: number; fresh: boolean}
e.json(["this", "is", "an", "array"]);
// unknown
EdgeDB also supports a special json
type for representing unstructured
data. Primitive data structures can be converted to JSON using a type cast
(<json>
). Alternatively, a properly JSON-encoded string can be converted
to json
with the built-in to_json
function. Indexing a json
value
returns another json
value.
edgedb>
select <json>5;
{"5"}
edgedb>
select <json>[1,2,3];
{"[1, 2, 3]"}
edgedb>
select to_json('[{ "name": "Peter Parker" }]');
{"[{\"name\": \"Peter Parker\"}]"}
edgedb>
select to_json('[{ "name": "Peter Parker" }]')[0]['name'];
{"\"Peter Parker\""}
/*
The result of an query returning `json` is represented
with `unknown` in TypeScript.
*/
e.json(5); // => unknown
e.json([1, 2, 3]); // => unknown
e.to_json('[{ "name": "Peter Parker" }]'); // => unknown
e.to_json('[{ "name": "Peter Parker" }]')[0]["name"]; // => unknown
Refer to Docs > EdgeQL > Literals for complete docs.
Functions and operators
EdgeDB provides a rich standard library of functions to operate and manipulate various data types.
db>
select str_upper('oh hi mark');
{'OH HI MARK'}
db>
select len('oh hi mark');
{10}
db>
select uuid_generate_v1mc();
{c68e3836-0d59-11ed-9379-fb98e50038bb}
db>
select contains(['a', 'b', 'c'], 'd');
{false}
e.str_upper("oh hi mark");
// string
e.len("oh hi mark");
// number
e.uuid_generate_v1mc();
// string
e.contains(["a", "b", "c"], "d");
// boolean
Similarly, it provides a comprehensive set of built-in operators.
db>
select not true;
{false}
db>
select exists 'hi';
{true}
db>
select 2 + 2;
{4}
db>
select 'Hello' ++ ' world!';
{'Hello world!'}
db>
select '😄' if true else '😢';
{'😄'}
db>
select <duration>'5 minutes' + <duration>'2 hours';
{<duration>'2:05:00'}
e.op("not", e.bool(true));
// booolean
e.op("exists", e.set("hi"));
// boolean
e.op("exists", e.cast(e.str, e.set()));
// boolean
e.op(e.int64(2), "+", e.int64(2));
// number
e.op(e.str("Hello "), "++", e.str("World!"));
// string
e.op(e.str("😄"), "if", e.bool(true), "else", e.str("😢"));
// string
e.op(e.duration("5 minutes"), "+", e.duration("2 hours"))
See Docs > Standard Library for reference documentation on all built-in types, including the functions and operators that apply to them.
Insert an object
Objects are created using insert
. The insert
statement relies on
developer-friendly syntax like curly braces and the :=
operator.
insert Movie {
title := 'Doctor Strange 2',
release_year := 2022
};
const query = e.insert(e.Movie, {
title: 'Doctor Strange 2',
release_year: 2022
});
const result = await query.run(client);
// {id: string}
// by default INSERT only returns
// the id of the new object
Nested inserts
One of EdgeQL’s greatest features is that it’s easy to compose. Nested inserts are easily achieved with subqueries.
insert Movie {
title := 'Doctor Strange 2',
release_year := 2022,
director := (insert Person {
name := 'Sam Raimi'
})
};
const query = e.insert(e.Movie, {
title: 'Doctor Strange 2',
release_year: 2022,
director: e.insert(e.Person, {
name: 'Sam Raimi'
})
});
const result = await query.run(client);
// {id: string}
// by default INSERT only returns
// the id of the new object
Select objects
Use a shape to define which properties to select
from the given object
type.
select Movie {
id,
title
};
const query = e.select(e.Movie, () => ({
id: true,
title: true
}));
const result = await query.run(client);
// {id: string; title: string; }[]
// To select all properties of an object, use the
// spread operator with the special "*"" property:
const query = e.select(e.Movie, () => ({
...e.Movie['*']
}));
Fetch linked objects with a nested shape.
select Movie {
id,
title,
actors: {
name
}
};
const query = e.select(e.Movie, () => ({
id: true,
title: true,
actors: {
name: true,
}
}));
const result = await query.run(client);
// {id: string; title: string, actors: {name: string}[]}[]
Filtering, ordering, and pagination
The select
statement can be augmented with filter
, order by
,
offset
, and limit
clauses (in that order).
select Movie {
id,
title
}
filter .release_year > 2017
order by .title
offset 10
limit 10;
const query = e.select(e.Movie, (movie) => ({
id: true,
title: true,
filter: e.op(movie.release_year, ">", 1999),
order_by: movie.title,
offset: 10,
limit: 10,
}));
const result = await query.run(client);
// {id: string; title: number}[]
Note that you reference properties of the object to include in your select
by prepending the property name with a period: .release_year
. This is known
as leading dot notation.
Every new set of curly braces introduces a new scope. You can add filter
,
limit
, and offset
clauses to nested shapes.
select Movie {
title,
actors: {
name
} filter .name ilike 'chris%'
}
filter .title ilike '%avengers%';
e.select(e.Movie, movie => ({
title: true,
characters: c => ({
name: true,
filter: e.op(c.name, "ilike", "chris%"),
}),
filter: e.op(movie.title, "ilike", "%avengers%"),
}));
// => { characters: { name: string; }[]; title: string; }[]
const result = await query.run(client);
// {id: string; title: number}[]
See Filtering, Ordering, and Pagination.
Query composition
We’ve seen how to insert
and select
. How do we do both in one query?
Answer: query composition. EdgeQL’s syntax is designed to be composable,
like any good programming language.
select (
insert Movie { title := 'The Marvels' }
) {
id,
title
};
const newMovie = e.insert(e.Movie, {
title: "The Marvels"
});
const query = e.select(newMovie, () => ({
id: true,
title: true
}));
const result = await query.run(client);
// {id: string; title: string}
We can clean up this query by pulling out the insert
statement into a
with
block. A with
block is useful for composing complex multi-step
queries, like a script.
with new_movie := (insert Movie { title := 'The Marvels' })
select new_movie {
id,
title
};
/*
Same as above.
In the query builder, explicit ``with`` blocks aren't necessary!
Just assign your EdgeQL subqueries to variables and compose them as you
like. The query builder automatically convert your top-level query to an
EdgeQL expression with proper ``with`` blocks.
*/
Computed properties
Selection shapes can contain computed properties.
select Movie {
title,
title_upper := str_upper(.title),
cast_size := count(.actors)
};
e.select(e.Movie, movie => ({
title: true,
title_upper: e.str_upper(movie.title),
cast_size: e.count(movie.actors)
}))
// {title: string; title_upper: string; cast_size: number}[]
A common use for computed properties is to query a link in reverse; this is known as a backlink and it has special syntax.
select Person {
name,
acted_in := .<actors[is Content] {
title
}
};
e.select(e.Person, person => ({
name: true,
acted_in: e.select(person["<actors[is Content]"], () => ({
title: true,
})),
}));
// {name: string; acted_in: {title: string}[];}[]
See Docs > EdgeQL > Select > Computed fields and Docs > EdgeQL > Select > Backlinks.
Update objects
The update
statement accepts a filter
clause up-front, followed by a
set
shape indicating how the matching objects should be updated.
update Movie
filter .title = "Doctor Strange 2"
set {
title := "Doctor Strange in the Multiverse of Madness"
};
const query = e.update(e.Movie, (movie) => ({
filter: e.op(movie.title, '=', 'Doctor Strange 2'),
set: {
title: 'Doctor Strange in the Multiverse of Madness',
},
}));
const result = await query.run(client);
// {id: string}
When updating links, the set of linked objects can be added to with +=
,
subtracted from with -=
, or overwritten with :=
.
update Movie
filter .title = "Doctor Strange 2"
set {
actors += (select Person filter .name = "Rachel McAdams")
};
e.update(e.Movie, (movie) => ({
filter: e.op(movie.title, '=', 'Doctor Strange 2'),
set: {
actors: {
"+=": e.select(e.Person, person => ({
filter: e.op(person.name, "=", "Rachel McAdams")
}))
}
},
}));
Delete objects
The delete
statement can contain filter
, order by
, offset
, and
limit
clauses.
delete Movie
filter .title ilike "the avengers%"
limit 3;
const query = e.delete(e.Movie, (movie) => ({
filter: e.op(movie.title, 'ilike', "the avengers%"),
}));
const result = await query.run(client);
// {id: string}[]
Query parameters
You can reference query parameters in your queries with $<name>
notation.
Since EdgeQL is a strongly typed language, all query parameters must be
prepending with a type cast to indicate the expected type.
Scalars like str
, int64
, and json
are
supported. Tuples, arrays, and object types are not.
insert Movie {
title := <str>$title,
release_year := <int64>$release_year
};
const query = e.params({ title: e.str, release_year: e.int64 }, ($) => {
return e.insert(e.Movie, {
title: $.title,
release_year: $.release_year,
}))
};
const result = await query.run(client, {
title: 'Thor: Love and Thunder',
release_year: 2022,
});
// {id: string}
All client libraries provide a dedicated API for specifying parameters when executing a query.
import {createClient} from "edgedb";
const client = createClient();
const result = await client.query(`select <str>$param`, {
param: "Play it, Sam."
});
// => "Play it, Sam."
import edgedb
client = edgedb.create_async_client()
async def main():
result = await client.query("select <str>$param", param="Play it, Sam")
# => "Play it, Sam"
package main
import (
"context"
"log"
"github.com/edgedb/edgedb-go"
)
func main() {
ctx := context.Background()
client, err := edgedb.CreateClient(ctx, edgedb.Options{})
if err != nil {
log.Fatal(err)
}
defer client.Close()
var (
param string = "Play it, Sam."
result string
)
query := "select <str>$0"
err = client.Query(ctx, query, &result, param)
// ...
}
// [dependencies]
// edgedb-tokio = "0.5.0"
// tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"] }
#[tokio::main]
async fn main() {
let conn = edgedb_tokio::create_client()
.await
.expect("Client initiation");
let param = "Play it, Sam.";
let val = conn
.query_required_single::<String, _>("select <str>$0", &(param,))
.await
.expect("Returning value");
println!("{val}");
}
Subqueries
Unlike SQL, EdgeQL is composable; queries can be naturally nested. This is useful, for instance, when performing nested mutations.
with
dr_strange := (select Movie filter .title = "Doctor Strange"),
benedicts := (select Person filter .name in {
'Benedict Cumberbatch',
'Benedict Wong'
})
update dr_strange
set {
actors += benedicts
};
// select Doctor Strange
const drStrange = e.select(e.Movie, movie => ({
filter: e.op(movie.title, '=', "Doctor Strange")
}));
// select actors
const actors = e.select(e.Person, person => ({
filter: e.op(person.name, 'in', e.set(
'Benedict Cumberbatch',
'Benedict Wong'
))
}));
// add actors to cast of drStrange
const query = e.update(drStrange, ()=>({
actors: { "+=": actors }
}));
We can also use subqueries to fetch properties of an object we just inserted.
with new_movie := (insert Movie {
title := "Avengers: The Kang Dynasty",
release_year := 2025
})
select new_movie {
title, release_year
};
// "with" blocks are added automatically
// in the generated query!
const newMovie = e.insert(e.Movie, {
title: "Avengers: The Kang Dynasty",
release_year: 2025
});
const query = e.select(newMovie, ()=>({
title: true,
release_year: true,
}));
const result = await query.run(client);
// {title: string; release_year: number;}
Polymorphic queries
Consider the following schema.
abstract type Content {
required title: str;
}
type Movie extending Content {
release_year: int64;
}
type TVShow extending Content {
num_seasons: int64;
}
We can select
the abstract type Content
to simultaneously fetch all
objects that extend it, and use the [is <type>]
syntax to select
properties from known subtypes.
select Content {
title,
[is TVShow].num_seasons,
[is Movie].release_year
};
const query = e.select(e.Content, (content) => ({
title: true,
...e.is(e.Movie, {release_year: true}),
...e.is(e.TVShow, {num_seasons: true}),
}));
/* {
title: string;
release_year: number | null;
num_seasons: number | null;
}[] */
Grouping objects
Unlike SQL, EdgeQL provides a top-level group
statement to compute
groupings of objects.
group Movie { title, actors: { name }}
by .release_year;
e.group(e.Movie, (movie) => {
const release_year = movie.release_year;
return {
title: true,
by: {release_year},
};
});
/* {
grouping: string[];
key: { release_year: number | null };
elements: { title: string; }[];
}[] */