EdgeDB v4
This release cycle is much shorter than the previous ones. It reflects our new approach at EdgeDB where the goal is to provide improvements at a steady regular pace rather than in big, but infrequent batches. Going forward we expect to maintain this shorter release cadence focusing on a few features at a time.
To play with the new features, install the CLI using our installation guide and initialize a new project.
$
edgedb project init
Upgrading
Local and Cloud instances
To upgrade a local project, first ensure that your CLI is up to date with
edgedb cli upgrade
. Then run the following command inside the project
directory.
$
edgedb project upgrade
Alternatively, specify an instance name if you aren’t using a project.
$
edgedb instance upgrade -I my_instance
The CLI will first check to see if your schema will migrate cleanly to EdgeDB 4.0. If the upgrade check finds any problems, it will report them back to you.
Hosted instances
To upgrade a remote (hosted) instance, we recommend the following dump-and-restore process.
-
EdgeDB v4.0 only supports PostgreSQL 14 (or above). So check the version of PostgreSQL you are using before upgrading EdgeDB. If you’re using Postgres 13 or below, you should upgrade Postgres first.
-
Spin up an empty 4.0 instance. You can use one of our deployment guides.
Under Debian/Ubuntu, when adding the EdgeDB package repository, use this command instead:
Copy$
echo deb [signed-by=/usr/local/share/keyrings/edgedb-keyring.gpg] \ https://packages.edgedb.com/apt \ $(grep "VERSION_CODENAME=" /etc/os-release | cut -d= -f2) main \ | sudo tee /etc/apt/sources.list.d/edgedb.list
Use this command for installation under Debian/Ubuntu:
Copy$
sudo apt-get update && sudo apt-get install edgedb-4
Under CentOS/RHEL, use this installation command:
Copy$
sudo yum install edgedb-4
In any required
systemctl
commands, replaceedgedb-server-3
withedgedb-server-4
.Under any Docker setups, supply the
4.0
tag. -
Take your application offline, then dump your v3.x database with the CLI
Copy$
edgedb dump --dsn <old dsn> --all --format dir my_database.dump/
This will dump the schema and contents of your current database to a directory on your local disk called
my_database.dump
. The directory name isn’t important. -
Restore the empty v4.x instance from the dump
Copy$
edgedb restore --all my_database.dump/ --dsn <new dsn>
Once the restore is complete, update your application to connect to the new instance.
This process will involve some downtime, specifically during steps 2 and 3.
If your Postgres cluster is also backing other versions of EdgeDB, make
sure you start your new instance with the --ignore-other-tenants
option when bootstrapping your new instance.
New features
Full-text Search
EdgeDB 4.0 adds full-text search functionality packaged
in the fts
module. By adding an fts::index
to an object type you can
transform any object into a searchable document:
type Item {
required available: bool {
default := false;
};
required name: str;
required description: str;
index fts::index on (
fts::with_options(
.name,
language := fts::Language.eng
)
);
}
The fts::index
indicates to EdgeDB that this object type is a valid target
for full-text search. The property that will be searched as well as the
language is provided in the index.
The fts::search()
function allows searching objects for a particular
phrase:
db>
select fts::search(Item, 'candy corn', language := 'eng');
{ ( object := default::Item {id: 9da06b18-69b2-11ee-96b9-1bedbe75ad4f}, score := 0.30396354, ), ( object := default::Item {id: 92375624-69b2-11ee-96b9-675b9b87ac70}, score := 0.6079271, ), }
The search results are provided as a tuple containing the matching document object and a score. Higher score indicates a better match. So we can use these values to order the results:
db> ... ... ... ...
with res := (
select fts::search(Item, 'candy corn', language := 'eng')
)
select res.object {name, score := res.score}
order by res.score desc;
{ default::Item {name: 'Candy corn', score: 0.6079271}, default::Item {name: 'Canned corn', score: 0.30396354}, }
You can only have at most one fts::index
defined for any particular type.
So if there are multiple properties that should be searchable, they can all be
specified in that one index:
type Item {
required available: bool {
default := false;
};
required name: str;
required description: str;
index fts::index on ((
fts::with_options(
.name,
language := fts::Language.eng
),
fts::with_options(
.description,
language := fts::Language.eng
)
));
}
The above schema declares both name
and description
as searchable
fields:
db> ... ... ... ...
with res := (
select fts::search(Item, 'trick or treat', language := 'eng')
)
select res.object {name, description, score := res.score}
order by res.score desc;
{ default::Item { name: 'Candy corn', description: 'A great Halloween treat', score: 0.30396354, }, }
Multiranges
We’ve made it easier to work with ranges by adding a multirange datatype. Multiranges consist of one or more ranges and allow
expressing intervals that are not contiguous. Multiranges are automatically
normalized to contain non-overlapping ranges that are ordered according to
their boundaries. All the usual range operators and functions like
overlaps
or contains
work with any combination of ranges and
multiranges, providing more flexibility in expressions.
db>
select multirange([range(8, 10)]) + range(1, 5) - range(3, 4);
{[range(1, 3), range(4, 5), range(8, 10)]}
GraphQL and HTTP authentication
Starting in rc1, the EdgeQL over HTTP and GraphQL endpoints support (and by default require) authentication.
By default, HTTP Basic Authentication is used.
Full details are available in the EdgeQL over HTTP documentation.
This is a backwards-incompatible change. It is possible to opt-in to the old behavior, but not recommended.
Extensions
auth
The new auth
extension adds a full authentication service that runs
alongside your database instance, saving you the hassle of having to learn and
implement the intricacies of OAuth or secure password storage.
-
OAuth Integration: Seamlessly authenticate with GitHub, Google, Apple, and Azure/Microsoft.
-
Email & Password Support: Includes robust email+password authentication with reset password functionality.
-
Easy Configuration: Set up via our configuration system.
-
Hosted UI: Use our hosted authentication UI to quickly add authentication to your app.
When a user signs up, we create a new object of type ext::auth::Identity
,
which you can link to in your own schema. We then provide you with a token that
can be set as the global ext::auth::client_token
which will automatically
populate another computed global called ext::auth::ClientTokenIdentity
which you can use directly in your access policies, or in your own globals.
using extension auth;
module default {
global current_customer := (
assert_single((
select Customer
filter .identity = global ext::auth::ClientTokenIdentity
))
);
type Customer {
required text: str;
required identity: ext::auth::Identity;
}
type Item {
required sku: str;
required description: str;
}
type Cart {
required customer: Customer;
multi items: Item {
quantity: int32;
};
access policy customer_has_full_access
allow all
using (global current_customer ?= .customer);
}
}
Here’s an example query using the TypeScript client:
import { createClient } from "edgedb";
declare const tokenFromAuthServer: string;
const client = createClient()
.withGlobals({
"ext::auth::client_token": tokenFromAuthServer
});
const carts = await client.query(`select Cart { * };`);
pgcrypto
We’ve added pgcrypto to our extensions. This exposes
digest
, hmac
, gen_salt
and crypt
functions for your hashing,
encrypting and salting needs.
db>
select ext::pgcrypto::digest('encrypt this', 'sha1');
{b'\x05\x82\xd8YLF\xe7\xd4\x12\x91\n\xdb$\xf1!v\xf9\xd4\x89\xc4'}
db>
select ext::pgcrypto::gen_salt('md5');
{'$1$FjNlXgX7'}
Standard algorithms are “md5”, “sha1”, “sha224”, “sha256”, “sha384” and “sha512”. Moreover, any digest algorithm OpenSSL supports is automatically picked up.
pg_trgm
The pg_trgm extension provides functionality used to determine string similarity, which makes it a good text search alternative for some use cases:
db> ... ...
with x := {'hello world', 'word hero', 'help the world'}
select res := (x, ext::pg_trgm::word_similarity(x, 'hello world'))
order by res.1 desc;
{('hello world', 1), ('help the world', 0.5), ('word hero', 0.35714287)}
Additional changes
Performance
We’ve made a few internal changes affecting performance, the biggest of which was rewriting EdgeQL parser in Rust. Overall we’ve manged to reduce the baseline server memory consumption by 40%.
EdgeQL
-
Add new style of
if
/then
/else
syntax. (#6074)Many people find it more natural to write “if … then .. else …” for conditional expressions because it mirrors the conditional statement from other familiar programming languages.
Copydb>
select if count(Object) > 0 then 'got data' else 'no data';
{'got data'}
-
Support conditional DML. (#6181)
It can be useful to be able to create, update or delete different objects based on some condition:
Copywith name := <str>$0, admin := <bool>$1 select if admin then ( insert AdminUser { name := name } ) else ( insert User { name := name } )
A different use-case of conditional DML is using a
coalesce
operator to express things like “select or insert if missing”:Copyselect (select User filter .name = 'Alice') ?? (insert User { name := 'Alice' });
-
Add
contains
for JSON so that it can be used withpg::gin
index. (#5910) -
Add
to_bytes()
to convertstr
intobytes
using UTF-8 encoding. (#5960) -
Add
to_str()
to convertbytes
intostr
using UTF-8 encoding. (#5960) -
Add
enc::base64_encode
andenc::base64_decode
functions. (#5963)Copydb>
select enc::base64_encode(b'hello');
{'aGVsbG8='}
Copydb>
select enc::base64_decode('aGVsbG8=');
{b'hello'}
-
Add
when
clause to triggers to enable them to be conditional. (#6184) -
Allow empty arrays without cast in
insert
. (#6218)
GraphQL
-
Change how globals are passed in GraphQL queries. (#5864)
Instead of using a separate
globals
field (which is non-standard), usevariables
to add a__globals__
object to pass the global variables.In order to ensure backwards compatibility, the old way of passing globals is still valid. In case both the new and the old methods are used the globals being passed in them must match or else the query will be rejected.
-
Fix GraphQL bug with objects without editable fields. (#6056)
-
Fix GraphQL issues with deeply nested modules. (#6056)
-
Fix GraphQL
__typename
for non-default modules and mutations. (#6035) -
Fix GraphQL fragments on types from non-default module. (#6035)
Bug fixes
-
Fix a casting bug for some aliased expressions. (#5788)
-
Fix cardinality inference of calls to functions with
optional
args. (#5867) -
Fix the undefined order of columns in SQL
COPY
. (#6036) -
Fix drop of union links when source has a subtype. (#6044)
-
Fix link deletion policies on links to union types. (#6033)
-
Fix deletion issues of aliases that use
with
(#6052) -
Make
id
of schema objects stable. (#6058) -
Allow computed pointers on types to omit link/property kind specification. (#6073)
-
Support
listen_ports
greater than 32767. (#6194) -
Fix migration issues with some overloaded indexes/constraints in SDL. (#6172)
-
Support DML on right hand side of coalesce expressions. (#6202)
-
Fix cardinality inference of polymorphic shape elements. (#6255)
-
Fix migration issue involving property defaults. (#6265)
-
Fix bugs in
set ... using
statements withassert_exists
and similar. (#6267) -
Fix cardinality bug when a property appears in multiple splats. (#6255)
-
Make comparison operators non-associative (#6327)
-
Fix an obscure parser bug caused by constant extraction (#6328)
-
Cap the size of sets in
multi
configuration values to128
(#6402)
4.1
4.2
-
Fix schema delta for RESET EXPRESSION (#6463)
-
Fix plain references to __old__ in rewrites (#6470)
-
Fix std::range in singleton mode (#6475)
-
Treat password reset as a verification event (#6504)
-
Fixes to auth redirect urls (#6469)
-
Fix SQL introspection of __fts_document__ (#6507)
-
Type check mutation rewrites at migration time (#6466)
Mutation rewrite’s
using
expression are now required to be of correct type when the rewrite is created. Up until now, it was possible to migrate to a schema that contained a rewrite rule that would always throw a type error when an object was being inserted or updated.This might be considered a breaking change, but as it is clearly a bug in user schema and as it could also be considered a bug in the compiler, we are fixing it in a minor version.
4.3
-
Fix non-rewritten tuple literals (#6521)
-
Support
connect_timeout
in backend DSN (#6531) -
Fix coalesced DML in FOR loops over objects (#6526)
-
Fix inserts silently failing when a
json->array
handles ‘null’ (#6544) -
Fix tracing of enums in type expressions (#6548)
-
Fix dumps with FTS indexes (#6560)
-
Allow indexes that use user-defined functions to actually get hit (#6551)
-
Fix reloading readiness state and JWT
*_list
files in multi-tenant mode (#6562) -
Handle OPTIONS in the extension path which is needed for CORS preflight (#6577)
-
Optimize the compiler and reduce time of an update test by ~52% (#6579)
-
Always retry system connection on any BackendError (#6588)
-
Properly support @source/@target on multi-link constraints (#6585)
-
Fix constant extraction’s interaction with the new if-then-else (#6591)
-
Fix migrating between aliased and non-aliased computeds (#6566)
-
Improve error message for illegal casts and parameters. (#6511)
-
Don’t eval ext::auth::ClientTokenIdentity on every row in a filter (#6607)
-
Expose __type__ via SQL adapter (#6519)
-
Fix some bugs involving union and coalescing of optional values (#6590)
-
Avoid doing a join when injecting type ids (#6601)
-
Generate better code for ?? with multi RHS or object type (#6532)
-
Fix pgast nullability inference for subqueries and coalesce (#6529)
-
Fix changing a pointer to be computed when there is a subtype (#6565)
4.4
4.5
-
Fix spurious delete/create of constraints with arguments in migrations (#6712)
-
Speed up introspection of system objects like Database and Role (#6665)
-
Fix some alter+rename combos on pointers in POPULATE MIGRATION (#6666)
-
Fix issue with schema changes when trigger depends on a rewrite (#6706)
-
Get verification data from request body (#6723)
-
Make overloading a link when omitting
link
keyword work (#6718)
4.6
-
Fix issues with empty sets leaking out of optional scopes (#6747)
-
Fix empty array literals in SQL adapter (#6806)
-
Fix duration/memory config in config objects (#6827)
-
Fix hanging backend pool with fast connect (#6813)
-
Fix changing index expressions in migrations (#6843)
-
Properly report errors involving newly created types (#6852)
-
Make constraint error details contain useful information for developers (#6796)
-
Fix DML coalesce inside of IF/ELSE (#6917)
-
Allow trailing comma for GROUP’s BY clause (#7002)
-
Fix computed single scalar globals (#6999)
-
Fix query cache dbver issue with concurrent DDL (#6819)
-
Several UI fixes
-
Update behaviour of datatable editor to save input by default on closing (edgedb/edgedb-ui/#325)
-
Store instance version with schema data, so schema is refreshed when instance is upgraded (edgedb/edgedb-ui/#333)
-
De-duplicate queries when navigating repl history with ctrl+up/down (edgedb/edgedb-ui/7916ee70)
-
Add max height to inline editor in dataview (edgedb/edgedb-ui/b2fedb72)
-
Always order
id
column first in dataview (edgedb/edgedb-ui/9a7c352e) -
Improve rendering of long/multiline strings in dataview changes preview (edgedb/edgedb-ui/13511ebd)
-
Fix link props in visual query builder (edgedb/edgedb-ui/42492465)
-
4.7
-
UI: revert a few changes that were meant for 5.x only.
-
Revert “Compile single globals into materialized CTEs”, since it introduced multiple bugs. These bugs were already fixed in 5.x. (#6613)
-
Add optional PKCE challenge in email verification (#7037)
-
Fix very slow parsing performance of large files with unrecoverable errors (#7046)
-
Fix update rewrites on types that are children of updated type (#7073)