Search
ctrl/
Ask AI
Light
Dark
System

Client

The Client class implements the basic functionality required to establish a connection to your database and execute queries.

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

In actuality, the client maintains a 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:

Copy
const edgedb = require("edgedb");

const client = edgedb.createClient();

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

Copy
import * as edgedb from "edgedb";

const client = edgedb.createClient();

Notice we didn’t pass any arguments into createClient. That’s intentional.

In development, we recommend using edgedb project init to create an instance and link it to your project directory. As long as you’re inside this directory, createClient() with auto-detect the project and connect to the associated instance automatically.

In production you should use environment variables to provide connection information to createClient. See the Connection parameters docs for details.

Clients can be configured using a set of immutable methods that start with with.

These methods return a new Client instance that shares a connection pool with the original client! This is important. Each call to createClient instantiates a new connection pool.

The code example below demonstrates all available configuration settings. The value specified below is the default value for that setting.

Copy
import {createClient, Duration, IsolationLevel} from "edgedb";

const baseClient = createClient();
const client = baseClient
  .withConfig({
    // 10 seconds
    session_idle_transaction_timeout: Duration.from({seconds: 10}),
    // 0 seconds === no timeout
    query_execution_timeout: Duration.from({seconds: 0}),
    allow_bare_ddl: "NeverAllow",
    allow_user_specified_id: false,
    apply_access_policies: true,
  })
  .withRetryOptions({
    attempts: 3,
    backoff: (attemptNo: number) => {
      // exponential backoff
      return 2 ** attemptNo * 100 + Math.random() * 100;
    },
  })
  .withTransactionOptions({
    isolation: IsolationLevel.Serializable, // only supported value
    deferrable: false,
    readonly: false,
  });

To execute a basic query:

Copy
const edgedb = require("edgedb");

const client = edgedb.createClient();

async function main() {
  const result = await client.query(`select 2 + 2;`);
  console.log(result); // [4]
}

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

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

The .query method always returns an array of results. It places no constraints on cardinality.

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

If you know your query will only return a single element, you can tell EdgeDB to expect a singleton result by using the .querySingle method. This is intended for queries that return zero or one elements. If the query returns a set with more than one elements, the Client will throw a runtime error.

Note that if you’re selecting an array or tuple, the returned value may still be an array.

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

Use queryRequiredSingle for queries that return exactly one element. If the query returns an empty set or a set with multiple elements, the Client will throw a runtime error.

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

The TypeScript signatures of these methods reflects their behavior.

Copy
await client.query<number>(`select 2 + 2;`);
// number[]

await client.querySingle<number>(`select 2 + 2;`);
// number | null

await client.queryRequiredSingle<number>(`select 2 + 2;`);
// number

The client 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

string

bytes

Uint8Array

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 client’s built-in type classes, refer to the reference documentation.

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.

Copy
await client.queryJSON(`select {1, 2, 3};`);
// "[1, 2, 3]"

await client.querySingleJSON(`select <int64>{};`);
// "null"

await client.queryRequiredSingleJSON(`select 3.14;`);
// "3.14"

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.

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

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

Copy
await client.execute(`
  insert Person { name := "Robert Downey Jr." };
  insert Person { name := "Scarlett Johansson" };
  insert Movie {
    title := <str>$title,
    actors := (
      select Person filter .name in {
        "Robert Downey Jr.",
        "Scarlett Johansson"
      }
    )
  }
`, { title: "Iron Man 2" });

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.

Copy
const INSERT_MOVIE = `insert Movie {
  title := <str>$title
}`
const result = await client.querySingle(INSERT_MOVIE, {
  title: "Iron Man"
});
console.log(result);
// {id: "047c5893..."}

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

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.

Copy
const result = await client.query(`
  insert Movie {
    title := <str>$title
  };
  insert Person {
    name := <str>$name
  };
`, {
  title: "Thor: Ragnarok",
  name: "Anson Mount"
});
// [{id: "5dd2557b..."}]

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

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.

Copy
const edgedb = require("edgedb");

const client = edgedb.createClient();

async function main() {
  await client.ensureConnected();
}

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

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

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 transaction object exposes query(), execute(), querySQL(), executeSQL(), and other query*() methods that clients expose, with the only difference that queries will run within the current transaction and can be retried automatically.

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:

Copy
const email = "timmy@edgedb.com"

await client.transaction(async tx => {
  await tx.execute(
    `insert User { email := <str>$email }`,
    { email },
  )

  await sendWelcomeEmail(email);

  await tx.execute(
    `insert LoginHistory {
      user := (select User filter .email = <str>$email),
      timestamp := datetime_current()
    }`,
    { email },
  )
})

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.

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 Client API will meet your needs.