Client
The Client
class implements the basic 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 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:
const edgedb = require("edgedb");
const client = edgedb.createClient();
If you’re using TypeScript or have ES modules enabled, you can use
import
syntax instead:
import * as edgedb from "edgedb";
const client = edgedb.createClient();
Connections
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.
Configuring clients
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.
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,
});
Running queries
To execute a basic query:
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.
const result = await client.query<number>(`select 2 + 2;`);
// number[]
.query
method
The .query
method always returns an array of results. It places no
constraints on cardinality.
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]
.querySingle
method
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.
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
.queryRequiredSingle
method
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.
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
Type conversion
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 |
|
Arrays |
|
Tuples |
|
Named tuples |
|
Enums |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
Ranges |
To learn more about the client’s built-in type classes, refer to the reference documentation.
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.
await client.queryJSON(`select {1, 2, 3};`);
// "[1, 2, 3]"
await client.querySingleJSON(`select <int64>{};`);
// "null"
await client.queryRequiredSingleJSON(`select 3.14;`);
// "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.
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.
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" });
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
.
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.
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.
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.
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.
const edgedb = require("edgedb");
const client = edgedb.createClient();
async function main() {
await client.ensureConnected();
}
Transactions
The most robust way to execute transactional code is to use
the transaction()
API:
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:
-
Transactions are executed atomically;
-
If a transaction fails due to retryable error (like a network failure or a concurrent update error), the transaction would be retried;
-
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:
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.
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 Client API will meet your needs.