February 27, 2025

Gel's new EdgeQL features and CLI workflows

With Gel 6, we're introducing several exciting features that focus on the developer experience. Read more to learn about some of the improvements we've made to the EdgeQL and our CLI.

We're constantly working to make developing complex applications with Gel more intuitive and efficient. At the heart of this effort is EdgeQL, our query language designed to be both powerful and approachable. The more expressive your queries and data models can be, the more you can offload to your data layer.

One of the restrictions of user-defined functions in EdgeQL has been that they could not contain DML statements like insert, update, or delete. With Gel 6.0, this restriction has been lifted! We expect that many people will find the function interface to data mutation a very useful abstraction when writing queries.

Copy
function create_admin_user(name: str, email: str) -> AdminUser
  using (
    insert User {
      name := name,
      email := email,
      role := UserRole.Admin,
    }
  );

Take a look at the documentation for User-Defined Functions for more details.

We've simplified the scoping rules for EdgeQL to make them more intuitive, aligning them with what feels natural when using EdgeQL in practice. In the process of our attempts to clean up some of the more obscure corners of the language, we've found that the existing path factoring behavior was a source of confusion and complexity.

By default, new projects will use the new scoping rules by setting the using future simple_scoping pragma in the generated default.gel schema file. Existing projects will continue to use the old behavior, but we will now produce warnings when a query is relying on the old path factoring behavior to make it easier to update your queries.

You can read more about the history, rationale, and the details of the new scoping rules in RFC 1027 outlining the changes.

Another simple but impactful quality of life feature added in 6.0 is support for string interpolation in EdgeQL.

Copy
with
  who := 'world',
  greeting := 'Hello',
select '\(greeting), \(who)';

Complex types like unions and intersections can now be used inline in more places that expect a type. Here are a few examples:

Copy
# Updates and deletes with complex types
update Timestamped[is User | Group]
set {
  updated_at := datetime_of_statement()
};

# Complex types in intersections
select Shape[is Circle | Triangle & HasRightAngle];
Copy
# Complex types in function arguments
function get_name(x: User | Organization) -> str
  using (x.name);

Powerful queries are only part of the equation. Another real challenge is seamlessly integrating your evolving data model with the rest of your development workflow, including the code generation tools we provide. That's why we've also improved the Gel CLI with new automation features that help keep your data layer in sync with your codebase.

There are a number of points in the development workflow where you may have some tasks that need to run at certain points based on state changes in your database schema, or the project instance. Some common examples include:

  • Code generation: When the schema changes, you may need to regenerate your code generation files, such as the TypeScript query builder, or the various generated query files we support across our different language clients.

  • Data fixture loading: When you initialize a new project, you may need to load some data into the database as part of the initialization process.

  • Test or type checking: When your schema changes, or when you switch branches, you may need to re-run tests or type checking to see how the changes affect your code.

and more!

To make it easier to handle these cases, you can now define hooks in your gel.toml file to run scripts in response to certain events, such as for schema changes, project initialization, branch switching, and more.

Copy
[instance]
server-version = "6.1.0"

[hooks]
schema.update.after = """
npx @gel/generate edgeql-js && npx @gel/generate queries
"""

See the gel.toml configuration file documentation for more details.

In version 3.0 we introduced a new watch command to the CLI to make it easier to iterate on your schema without having to manually create and apply multiple migrations. After living with this feature for a while, we've found that the need to respond to changes in the filesystem to trigger database related side-effects is a more general need than just for schema changes.

While designing the hooks feature detailed above, we realized that the other "half" of the equation for ensuring that schema changes and query changes are kept in sync is to be able to trigger code generation when query files change. We decided that a general purpose solution for this would be a great addition to the CLI, and so we've changed the watch mode which can now be configured to watch glob patterns and trigger arbitrary commands.

Here's an example of how you can use the new watch mode to trigger code generation when query files change, making it a perfect complement to the hooks TypeScript code generation example we showed above.

Copy
Show 5 hidden lines...
npx @gel/generate edgeql-js && npx @gel/generate queries
"""

[[watch]]
files = ["**/*.edgeql"]
script = "npx @gel/generate queries"

The old behavior of applying migrations in dev-mode is still available by passing the --migrate flag to the watch command.

ShareTweet