July 28, 2022

EdgeDB 2.0

Check out the discussion of this post on Hacker News. See the recording of the live launch event on YouTube.

Today, just under 6 months after the release of 1.0 🏁, we’re excited to announce EdgeDB 2.0.

Since our 1.0 release in February, we’ve published 3 minor versions in the 1.x line, started a Discord (750 members and counting!), accumulated a few thousand more GitHub stars, and grown to thousands of active users. We couldn’t be happier with how things have gone—and we have some crazy exciting stuff in the pipeline.

Back to 2.0. This release brings a slew of new features, including:

  • a built-in admin UI;

  • a top-level GROUP statement in EdgeQL;

  • object-level security;

  • range types;

  • an official Rust client library (at long last!)

  • …and a lot more.

…let’s briefly discuss why to use EdgeDB as the database for your next project.

EdgeDB is a new database that extends the relational model to eliminate the object-relational impedance mismatch. This lets you model and query your data in a more intuitive object-oriented way, while retaining the reliability and performance of the classic relational model. We call it a graph-relational database.

By natively bridging the object-relational gap, EdgeDB eliminates the need for additional tooling like ORMs and middleware to manage data modeling, migrations, querying, and middleware. These are a de facto necessity when building applications with a traditional relational database. EdgeDB eliminates unnecessary layers from your software stack, enables powerful querying that’s impossible with ORMs and impractical with SQL, and brings significant runtime performance benefits.

The full list of EdgeDB features printed on a thin strip of paper would probably stretch to the moon and back. But let’s enumerate some of the biggest ones:

A declarative schema

…which lets you express computed properties, inheritance, functions, complex constraints and indexes, and access control rules.

Copy
type User {
  required property email -> str {
    constraint exclusive;
  }
}

type BlogPost {
  required property title -> str;
  required property published -> bool {
    default := false
  };
  link author -> User;

  index on (.title);
}

A builtin migration system

…consisting of a database-native migration planner, automatic migration history tracking, and a CLI-based workflow.

Copy
$ 
edgedb migration create
Created dbschema/migrations/00001.edgeql
Copy
$ 
edgedb migrate
Applied dbschema/migrations/00001.edgeql

A modern, lean query language

…that matches the expressive power of SQL while remaining more composable and less verbose (and eliminating JOIN!)

Copy
select BlogPost {
  title,
  trimmed_title := str_trim(.title),
  author: {
    email
  }
}
filter not .published

A TypeScript query builder

…that can express arbitrary EdgeQL queries and automatically infer the query return type.

Copy
e.select(e.BlogPost, post => ({
  title: true,
  trimmed_title: e.str_trim(post.title),
  author: {
    email: true
  },
  filter: e.op("not", post.published)
}))

And it’s 100% open source and powered by Postgres under the hood.

For a more philosophical treatment of our motivations for building EdgeDB, check out our EdgeDB 1.0 announcement. In the meantime, let’s talk about 2.0.

EdgeDB 2.0 improvements touch every aspect of the database: the type system, the query language, the client libraries and binary protocol, and the developer experience of building an app with EdgeDB.

Refer to the v2.0 changelog for an overly detailed list of features and fixes and instructions for upgrading your projects to EdgeDB 2.0.

EdgeDB UI is a beautiful, feature-rich admin panel baked directly into all EdgeDB 2.0+ instances. Open it by running edgedb ui in your project directory, which will open a localhost page in your default browser. It ships with:

  • a data browser and editor;

  • a REPL for writing and executing EdgeQL queries;

  • a schema introspection tool with text-based and graphical visualizations of the schema.

Use EdgeDB UI to populate your new database with some test data, debug that complex EdgeQL query, or scroll around your schema graph admiringly.

Needless to say that this is just the beginning. In the future releases we’ll continue to add new features to the UI, like a query plan visualizer, built-in documentation, and administration tools.

The new top-level GROUP statement can be used to partition and aggregate data. The output of GROUP is a set of objects—just like any old SELECT query. Each object represents a group and contains three fields: grouping, key, and elements. Here’s a simple example:

Copy
db> 
group Movie by .release_year;
{
  {
    key: {release_year: 2016},
    grouping: {'release_year'},
    elements: {
      default::Movie {title: 'Captain America: Civil War'},
      default::Movie {title: 'Doctor Strange'},
    },
  },
  {
    key: {release_year: 2017},
    grouping: {'release_year'},
    elements: {
      default::Movie {title: 'Guardians of the Galaxy Vol. 2'},
      default::Movie {title: 'Spider-Man: Homecoming'},
      default::Movie {title: 'Thor: Ragnarok'},
    },
  },
  ...
}

You can also group by arbitrary EdgeQL expressions, nested fetch properties and links on elements, and run complex analytical queries with grouping sets (hello CUBE and ROLLUP!). Though its true power lies in its ability to compose with the rest of the language:

Copy
db> 
... 
... 
... 
... 
... 
... 
... 
... 
... 
... 
... 
with
  groups := (
    group Movie
    using
      starts_with_vowel := re_test('(?i)^[aeiou]', .title),
    by starts_with_vowel
  )
select groups {
  starts_with_vowel := .key.starts_with_vowel,
  count := count(.elements),
  mean_title_length := math::mean(len(.elements.title))
};
{
  {starts_with_vowel: false, count: 12, mean_title_length: 19.75},
  {starts_with_vowel: true, count: 3, mean_title_length: 19.66},
}

In SQL, GROUP BY is a clause tacked onto the end of the SELECT statement that dramatically changes the intent of the query and imposes a list of requirements. (For instance, all columns other than the grouped keys are to be referenced only as arguments to aggregate functions.) By contrast, the EdgeQL syntax allows for frictionless composability, giving it an edge (😘) over SQL.

EdgeDB’s powerful schema currently supports a full set of primitive datatypes, descriptive object syntax, type mixins, dynamically computed properties and links, complex constraints and indexes, user-defined functions, and more. It’s more than capable of representing a data model of any complexity.

With EdgeDB 2.0, we’re taking this to the next level. With object-level security you can implement your application’s access control logic at the schema level. EdgeDB will transparently enforce it everywhere and act as a single source of truth in your infrastructure.

In practice, this means adding access policies to your object types that restrict the set of objects that can be selected, inserted, updated, or deleted for a particular type. Let’s start with a simple blog schema with no access policies.

Copy
type User {
  required property email -> str {
    constraint exclusive;
  };
}

type BlogPost {
  required property title -> str;
  link author -> User;
}

We’re going to add an access policy to ensure that posts can only be updated by their author. But how do we communicate to the database who is executing a particular query?

Copy
  global current_user -> uuid;

  type User {
    required property email -> str {
      constraint exclusive;
    };
  }

  type BlogPost {
    required property title -> str;
    link author -> User;
  }

We add a global variable called current_user. Global variables are a new mechanism for defining a context for query execution. Once a global has been declared in your schema, you can easily set a value for it via the client API or in REPL.

TypeScript
Python
EdgeQL
Copy
import createClient from 'edgedb';

const client = createClient();

const myApiHandler = async (userId: string) => {
  const scopedClient = client.withGlobals({
    current_user: userId,
  });

  return await scopedClient.query(
    `select global current_user;`
  );
}
Copy
import edgedb

client = edgedb.create_client()

async def my_api_handler(user_id):
    scoped_client = client.with_globals({
        'current_user': user_id,
    })

    return await scoped_client.query(
      "select global current_user;"
    )
Copy
db> 
... 
set global current_user :=
  (SELECT User FILTER .email = 'elvis@edgedb.com').id;
OK: SET GLOBAL
Copy
db> 
select global current_user;
{<uuid>"5b4d1530-0e0b-11ed-ae2a-133197f4faf5"}

Our client libraries efficiently pack and transfer global variables as well as transparently handle the necessary client-side connection pooling and protocol-level magic for you.

Importantly (and unlike query parameters) you can reference global variables in any context, including in your schema. Let’s use current_user to add a new access policy to BlogPost.

Copy
  global current_user -> uuid;

  type User {
    required property email -> str {
      constraint exclusive;
    };
  }

  type BlogPost {
    required property title -> str;
    link author -> User;
    access policy own_posts
       allow all
       using (.author.id ?= global current_user)
  }

The new access policy is called own_posts and it allows all operations—select, update, insert, delete—if the post’s .author.id property is equal to the value of current_user global variable.

We are excited about how flexible the design of access policies turned out to be:

  • Policies can allow or deny access to specific operations, including select, insert, delete, and update (which can be further subdivided into update read and update write);

  • The using expression can correspond to an arbitrary EdgeQL expression;

  • You can add as many policies as you like.

This is a new versatile mechanism that can be used to implement a broad range of access logic. Here’s an example on how to use access policies to implement a temporal mixin type:

Copy
abstract type Temporal {
  required property validity_period -> range<datetime>;

  access policy hide_invalid allow all using (
    contains(.validity_period, datetime_of_transaction())
  )
}

Refer to the documentation for more details and examples.

Rust. The official Rust client is here, finally! 🎉 While we are using Rust pretty extensively (e.g. our CLI is 100% Rust), it took us some time to figure out and tune the API design of the client library. This brings the list of first-party client libraries to 4, including TypeScript, Python, and Go, along with community-maintained packages for .NET and Elixir.

Protocol. EdgeDB’s binary protocol reached version 1.0, receiving multiple enhancements:

  • Fully stateless: this enables having multiple concurrent sessions within one connection, as well as tunneling the protocol over HTTP. The HTTP tunneling is already used to implement the REPL experience of the new UI, and can later be used to enable new experiences, such as integration with environments like Next.js Live.

  • Support for global variables and local state. Upon connecting clients receive the full state descriptor to be able to serialize values for global variables and configuration.

  • Optimized parse/execute flow. Clients require even fewer back and forth communications with the server, improving latency.

Efficient local development. EdgeDB 2.0 supports socket activation for development instances, which means that it doesn’t run on your devbox until you actually try to use it. And when you do use it, all internal process pools autoscale to use the minimal amount of resources. This conserves your machine’s RAM and CPU and allows you to work on multiple EdgeDB projects without experiencing any slowdowns.

Ranges. EdgeDB 2.0 supports range types that can represent intervals of values, such as date/time values or 64-bit integers. Ranges implement a number of operators and built-in functions, and support casting to JSON and back.

Date/time. Date/time API was tweaked to make arithmetic on local date and local time more sound. A new cal::date_duration type was added along with a several new helper functions.

The list continues, read it in full in the v2 changelog!

We plan to release EdgeDB 3.0 in about 6 months. We’re sticking to a fast release cadence because there’s so much we want to do! Currently topping the todo list:

  • EXPLAIN command to analyze EdgeQL queries;

  • user-definable error types (exceptions);

  • support for splats in EdgeQL shapes;

  • role-based access control;

  • full-text search support.

Last, but not least, we are close to launching a preview of EdgeDB Cloud, a fully-managed hosted EdgeDB service that will make it possible to spin up production-ready instances with a single CLI command. Sign up below to be the first to experience it. ⛅

In the meantime, join us on Discord, give us a star on GitHub, and—most importantly—go build something wonderful with EdgeDB! ❤️