October 21, 2022

Typesafe database querying via code generation

As statically typed languages grow more widespread, so too does a common problem: how does one execute a database query in a typesafe way?

We’ve just published the v1.0 versions of our client libraries for JavaScript and Python, which include the generation features described in this post.

This consideration drives many developers towards using ORM libraries, which provide a subset of the functionality of the underlying query langauge and often come with performance tradeoffs. However, the ability to write strongly typed queries in a DRY way is often worth the tradeoff.

EdgeDB now supports a mechanism that provides the best of both worlds: typesafe code generated directly from your EdgeQL queries.

The word “typesafe” means different things to different people, so let’s be clear what we mean here. EdgeQL queries can contain parameters—inputs—designated with a $ prefix. They also are statically analyzed by the EdgeDB server, so their expected return type is known ahead of time.

The goal is to provide a mechanism by which queries are written in EdgeQL and converted to a “language-native” form such that our statically-typed language of choice—be it TypeScript, Dart, or Python (via dataclasses)— enforces the input types and returns a typed result at compile-time.

The natural way to achieve this is to represent our queries as typed functions.

Our official client libraries for JavaScript/TypeScript, Python, and Dart have all been updated to include a mechanism to perform the following steps.

  • Detect *.edgeql files in your project

  • Send the contents to the running EdgeDB instance

  • Get back rich type descriptions of the queries parameters and return types

  • Generate a source file containing an appropriately typed function

Our official .NET client is coming soon, and will also support this workflow. Stay tuned!

Let’s look at the TypeScript workflow for demonstration purposes. Assume a file called getMovie.edgeql exists in your project directory with the following contents:

Copy
# getMovie.edgeql
select Movie {
  title,
  release_year
}
filter .title = <str>$title

Install the codegen package from NPM, then run the generation command.

Copy
$ 
npm install @edgedb/generate -D
Copy
$ 
npx @edgedb/generate queries

This will generate a file getMovies.edgeql.ts alongside the getMovie.edgeql file, containing the following fully-typed function:

Copy
import type {createClient} from "edgedb";

async function getMovie(
  client: Client,
  params: {title: string}
): Promise<{ title: string; release_year: number | null }> {
  return client.querySingle(`
    select Movie {
      title,
      release_year
    }
    filter .title = <str>$title
  `, params);
}

This generated “query function” can be imported and used in your application like so:

Copy
import {getMovie} from "./getMovie.edgeql.ts"; // importing

const movie = await getMovie(client, {
  title: "Avengers: The Kang Dynasty"
});

Support for code generation from query files has been added to our client libraries for JavaScript/TypeScript, Python, and Dart. Different language ecosystems provide different idioms and mechanisms for code generation, so the precise workflow varies.

First, initialize a project and install the client library for your chosen language, plus any additional dependencies.

Node.js
Deno
Python
Dart
Copy
$ 
yarn add edgedb
Copy
$ 
yarn add @edgedb/generate -D  # dev dependency
Copy
$ 
n/a
Copy
$ 
pip install edgedb # must be 1.0 or later!
Copy
$ 
dart pub add edgedb
Copy
$ 
dart pub add --dev build_runner

Then run the code generator. For each detected *.edgeql file, an appropriate source file will be generated alongside it. For instance a file getMovie.edgeql will result in getMovie.edgeql.ts.

  • As needed, certain generators support multiple targets, such as --target {async|blocking} in edgedb-python.

  • Where possible, some generators support a “single-file mode” in which all query functions are aggregated into a single generated file.

  • Where possible, some generators support a “watch mode” that listens for changes to *.edgeql files and regenerates the source files as needed.

Node.js
Deno
Python
Dart
Copy
$ 
npx @edgedb/generate queries
Copy
$ 
npx @edgedb/generate queries --file  # all functions in a single file
Copy
$ 
deno run --allow-all --unstable https://deno.land/x/edgedb/generate.ts queries
Copy
$ 
deno run --allow-all --unstable https://deno.land/x/edgedb/generate.ts queries --file  # all functions in a single file
Copy
$ 
edgedb-py
Copy
$ 
edgedb-py --target {async|blocking|pydantic} # async is the default
Copy
$ 
edgedb-py --file  # all functions in a single file
Copy
$ 
dart run build_runner build
Copy
$ 
dart run build_runner watch # watch mode

For specifics for your preferred language, refer to the library-specific documentation below.

As part of a larger effort towards encouraging codegen-based workflows, our JavaScript/TypeScript client now exposes a set of tools to make it easier to build your own third-party code generators. The *.edgeql workflow described here and our TypeScript query builder are both implemented using this same set of tools.

After installing edgedb@1.x+ from NPM, use the $ variable to access these tools.

Copy
import {$, createClient} from "edgedb";

const client = createClient();

// get the TS representation of a query's parameters and result type
await $.analyzeQuery(client, `<query here>`);

// introspect the entire typesystem
await $.introspect.types(client);

// additional introspection tools
await $.introspect.functions(client);
await $.introspect.operators(client);
await $.introspect.casts(client);
await $.introspect.scalars(client);

For advanced use cases, you can fall back to hand-writing introspection queries. The $.introspect tools are just a convenient wrapper around these queries that provide a fully typed result.

Use the information returned by these tools to generate whatever source code you need: React hooks, GraphQL resolvers, Zod schemas, etc. If you build a generator, let us know on our Discord so we can list it in on the Generators page!