EdgeQL
EdgeQL is the query language of Gel. 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 Gel. Refer to the linked pages for more in-depth documentation.
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
Gel 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 ❤️ EdgeQL"; # str{'U ❤️ EdgeQL'}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 ❤️ EdgeQL")
// 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"))
// BufferOther 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")
// gel.Duration (custom class)
e.cal.relative_duration("2 years 18 days")
// gel.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 tupledb>
select (fruit := 'Apple', quantity := 3.14, fresh := true);{(fruit := 'Apple', quantity := 3.14, fresh := true)} # named tupledb>
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"]);
// unknownGel 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.
gel>
select <json>5;{"5"}gel>
select <json>[1,2,3];{"[1, 2, 3]"}gel>
select to_json('[{ "name": "Peter Parker" }]');{"[{\"name\": \"Peter Parker\"}]"}gel>
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"]; // => unknownRefer to Docs > EdgeQL > Literals for complete docs.
Functions and operators
Gel 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");
// booleanSimilarly, 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 objectNested 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 objectSelect 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 "gel";
const client = createClient();
const result = await client.query(`select <str>$param`, {
param: "Play it, Sam."
});
// => "Play it, Sam."import gel
client = gel.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/geldata/gel-go"
)
func main() {
ctx := context.Background()
client, err := gel.CreateClient(ctx, gel.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]
// gel-tokio = "0.5.0"
// tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"] }
#[tokio::main]
async fn main() {
let conn = gel_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; }[];
}[] */