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; }[];
}[] */