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. impoart * 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()

cal::local_date

LocalDate()

cal::local_time

LocalTime()

cal::local_datetime

LocalDateTime()

cfg::memory

ConfigMemory()

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.

.query method​

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]

.querySingle method​

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

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

.queryRequiredSingle method​

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.querySingle<number>(`select 2 + 2;`);
  2. // number | null
  3. await client.queryRequiredSingle<number>(`select 2 + 2;`);
  4. // number

JSON results​

There are dedicated methods for running queries and retrieving results as a serialized JSON string. This serialization happens inside the database.

  1. await client.query(`select {1, 2, 3};`);
  2. // "[1, 2, 3]"
  3. await client.querySingle(`select <int64>{};`);
  4. // "null"
  5. await client.queryRequiredSingle(`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. };`);

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.

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.