Driver

The driver implements the core functionality required to establish a connection to your database and execute queries.

Creating clients

A client represents a connection to your database and provides methods for executing queries.

In actuality, the client maintains an pool of connections under the hood. When your server is under load, queries will be run in parallel across many connections, instead of being bottlenecked by a single connection.

To create a client:

  1. const edgedb = require("edgedb");
  2. const client = edgedb.createClient();

If you’re using TypeScript or have ES modules enabled, you can use import syntax instead:

  1. import * as edgedb from "edgedb";
  2. const client = edgedb.createClient();

Configuring the connection

Notice we didn’t pass any arguments into createClient. That’s intentional; we recommend using EdgeDB projects or environment variables to configure your database connections. See the Client Library Connection docs for details on configuring connections.

Running queries

To execute a basic query:

  1. const edgedb = require("edgedb");
  2. const client = edgedb.createClient();
  3. async function main() {
  4. const result = await client.query(`select 2 + 2;`);
  5. console.log(result); // [4]
  6. }

In TypeScript, you can supply a type hint to receive a strongly typed result.

  1. const result = await client.query<number>(`select 2 + 2;`);
  2. // number[]

Type conversion

The driver converts EdgeDB types into a corresponding JavaScript data structure. Some EdgeDB types like duration don’t have a corresponding type in the JavaScript type system, so we’ve implemented classes like Duration() to represent them.

EdgeDB type

JavaScript type

Sets

Array

Arrays

Array

Tuples tuple<x, y, …>

Array

Named tuples tuple<foo: x, bar: y, …>

object

Enums

string

Object

object

str

string

bool

boolean

float32 float64 int16 int32 int64

number

json

string

uuid

string

bigint

BigInt

decimal

N/A (not supported)

bytes

Buffer

datetime

Date

duration

Duration()

e.cal.relative_duration

RelativeDuration()

e.cal.date_duration

DateDuration()

cal::local_date

LocalDate()

cal::local_time

LocalTime()

cal::local_datetime

LocalDateTime()

cfg::memory

ConfigMemory()

Ranges range<x>

Range()

To learn more about the driver’s built-in type classes, refer to the reference documentation.

Enforcing cardinality

There are additional methods for running queries that have an expected cardinality. This is a useful way to tell the driver how many elements you expect the query to return.

The query method places no constraints on cardinality. It returns an array, no matter what.

  1. await client.query(`select 2 + 2;`); // [4]
  2. await client.query(`select <int64>{};`); // []
  3. await client.query(`select {1, 2, 3};`); // [1, 2, 3]

Use querySingle if you expect your query to return zero or one elements. Unlike query, it either returns a single element or null. Note that if you’re selecting an array, tuple, or set, the returned ‘single’ element will be an array.

  1. await client.querySingle(`select 2 + 2;`); // [4]
  2. await client.querySingle(`select <int64>{};`); // null
  3. await client.querySingle(`select {1, 2, 3};`); // Error

Use queryRequiredSingle for queries that return exactly one element.

  1. await client.queryRequiredSingle(`select 2 + 2;`); // 4
  2. await client.queryRequiredSingle(`select <int64>{};`); // Error
  3. await client.queryRequiredSingle(`select {1, 2, 3};`); // Error

The TypeScript signatures of these methods reflects their behavior.

  1. await client.query<number>(`select 2 + 2;`);
  2. // number[]
  3. await client.querySingle<number>(`select 2 + 2;`);
  4. // number | null
  5. await client.queryRequiredSingle<number>(`select 2 + 2;`);
  6. // number

JSON results

Client provide additional methods for running queries and retrieving results as a serialized JSON string. This serialization happens inside the database and is typically more performant than running JSON.stringify yourself.

  1. await client.queryJSON(`select {1, 2, 3};`);
  2. // "[1, 2, 3]"
  3. await client.querySingleJSON(`select <int64>{};`);
  4. // "null"
  5. await client.queryRequiredSingleJSON(`select 3.14;`);
  6. // "3.14"

Non-returning queries

To execute a query without retrieving a result, use the .execute method. This is especially useful for mutations, where there’s often no need for the query to return a value.

  1. await client.execute(`insert Movie {
  2. title := "Avengers: Endgame"
  3. };`);

With EdgeDB 2.0 or later, you can execute a “script” consisting of multiple semicolon-separated statements in a single .execute call.

  1. await client.execute(`
  2. insert Person { name := "Robert Downey Jr." };
  3. insert Person { name := "Scarlett Johansson" };
  4. insert Movie {
  5. title := <str>$title,
  6. actors := (
  7. select Person filter .name in {
  8. "Robert Downey Jr.",
  9. "Scarlett Johansson"
  10. }
  11. )
  12. }
  13. `, { title: "Iron Man 2" });

Parameters

If your query contains parameters (e.g. $foo), you can pass in values as the second argument. This is true for all query* methods and execute.

  1. const INSERT_MOVIE = `insert Movie {
  2. title := <str>$title
  3. }`
  4. const result = await client.querySingle(INSERT_MOVIE, {
  5. title: "Iron Man"
  6. });
  7. console.log(result);
  8. // {id: "047c5893..."}

Remember that parameters can only be scalars or arrays of scalars.

Scripts

Both execute and the query* methods support scripts (queries containing multiple statements). The statements are run in an implicit transaction (unless already in an explicit transaction), so the whole script remains atomic. For the query* methods only the result of the final statement in the script will be returned.

  1. const result = await client.query(`
  2. insert Movie {
  3. title := <str>$title
  4. };
  5. insert Person {
  6. name := <str>$name
  7. };
  8. `, {
  9. title: "Thor: Ragnarok",
  10. name: "Anson Mount"
  11. });
  12. // [{id: "5dd2557b..."}]

For more fine grained control of atomic exectution of multiple statements, use the transaction() API.

Checking connection status

The client maintains a dynamically sized pool of connections under the hood. These connections are initialized lazily, so no connection will be established until the first time you execute a query.

If you want to explicitly ensure that the client is connected without running a query, use the .ensureConnected() method.

  1. const edgedb = require("edgedb");
  2. const client = edgedb.createClient();
  3. async function main() {
  4. await client.ensureConnected();
  5. }

Transactions

The most robust way to execute transactional code is to use the transaction() API:

  1. await client.transaction(tx => {
  2. await tx.execute("insert User {name := 'Don'}");
  3. });

Note that we execute queries on the tx object in the above example, rather than on the original client object.

The transaction() API guarantees that:

  1. Transactions are executed atomically;

  2. If a transaction fails due to retryable error (like a network failure or a concurrent update error), the transaction would be retried;

  3. If any other, non-retryable error occurs, the transaction is rolled back and the transaction() block throws.

The key implication of retrying transactions is that the entire nested code block can be re-run, including any non-querying JavaScript code. Here is an example:

  1. const email = "timmy@edgedb.com"
  2. await client.transaction(async tx => {
  3. await tx.execute(
  4. `insert User { email := <str>$email }`,
  5. { email },
  6. )
  7. await sendWelcomeEmail(email);
  8. await tx.execute(
  9. `insert LoginHistory {
  10. user := (select User filter .email = <str>$email),
  11. timestamp := datetime_current()
  12. }`,
  13. { email },
  14. )
  15. })

In the above example, the welcome email may be sent multiple times if the transaction block is retried. Generally, the code inside the transaction block shouldn’t have side effects or run for a significant amount of time.

Transactions allocate expensive server resources and having too many concurrently running long-running transactions will negatively impact the performance of the DB server.

Next up

If you’re a TypeScript user and want autocompletion and type inference, head over to the Query Builder docs. If you’re using plain JavaScript that likes writing queries with composable code-first syntax, you should check out the query builder too! If you’re content writing queries as strings, the vanilla driver API will meet your needs.