Easy EdgeDB · Chapter 1

Jonathan Harker travels to Transylvania

Object TypesSelectInsert

In the beginning of the book we see the main character Jonathan Harker, a young lawyer who is going to meet a client. The client is a rich man named Count Dracula who lives somewhere in Eastern Europe. Jonathan doesn’t yet know that Count Dracula is a vampire, so he’s enjoying the trip to a new part of Europe. The book begins with Jonathan writing in his journal as he travels. The parts that are good for a database are in bold:

3 May. Bistritz.—Left Munich at 8:35 P.M., on 1st May, arriving at Vienna early next morning; should have arrived at 6:46, but train was an hour late. Buda-Pesth seems a wonderful place, from the glimpse which I got of it from the train…

This is already a lot of information, and it helps us start to think about our database schema. The language used for EdgeDB is called EdgeQL, and is used to define, mutate, and query data. Inside it is SDL (schema definition language) that makes migration easy, and which we will learn in this book. So far our schema needs the following:

  • Some kind of City or Location type. These types that we can create are called object types, made out of properties and links. What properties should a City type have? Perhaps a name and a location, and sometimes a different name or spelling. Bistritz for example is now called Bistrița (it’s in Romania), and Buda-Pesth is now written Budapest.

  • Some kind of Person type. We need it to have a name, and also a way to track the places that the person visited.

To make a type inside a schema, just use the keyword type followed by the type name, then {} curly brackets. Our Person type will start out like this:

Copy
type Person {
}

That’s all you need to create a type, but there’s nothing inside there yet. Inside the brackets we add the properties for our Person type. Use required property if the type needs it, and just property if it is optional.

Copy
type Person {
  required property name -> str;
  property places_visited -> array<str>;
}

With required property name our Person objects are always guaranteed to have a name - you can’t make a Person object without it. Here’s the error message if you try:

MissingRequiredError: missing value for required property default::Person.name

A str is just a string, and goes inside either single quotes: 'Jonathan Harker' or double quotes: "Jonathan Harker". The \ escape character before a quote makes EdgeDB treat it like just another letter: 'Jonathan Harker\'s journal'.

An array is a collection of the same type, and our array here is an array of strs. We want it to look like this: ["Bistritz", "Vienna", "Buda-Pesth"]. The idea is to easily search later and see which character has visited where.

places_visited is not a required property because we might later add minor characters that don’t go anywhere. Maybe one person will be the “innkeeper_in_bistritz” or something, and we won’t know or care about places_visited for him.

Now for our City type:

Copy
type City {
  required property name -> str;
  property modern_name -> str;
}

This is similar, just properties with strings. The book Dracula was published in 1897 when spelling for cities was sometimes different. All cities have a name in the book (that’s why it’s required), but some won’t need a different modern name. Vienna is still Vienna, for example. We are imagining that our game will link the city names to their modern names so we can easily place them on a map.

We haven’t created our database yet, though. There are two small steps that we need to do first after installing EdgeDB. First we create a database with the CREATE DATABASE keyword and our name for it:

Copy
CREATE DATABASE dracula;

Then we type \c dracula to connect to it.

Lastly, we we need to do a migration. This will give the database the structure we need to start interacting with it. Migrations are not difficult with EdgeDB:

  • First you start them with START MIGRATION TO {}

  • Inside this you add at least one module, so your types can be accessed. A module is a namespace, a place where similar types go together. The part on the left side of the :: is the name of the module, and the type inside is to the right. If you wrote module default and then type Person, the type Person would be at default::Person. So when you see a type like std::bytes for example, this means the type bytes inside std (the standard library).

  • Then you add the types we mentioned above, and finish up the block by ending with a }. Then outside of that, type POPULATE MIGRATION to add the data.

  • Finally, you type COMMIT MIGRATION and the migration is done.

There are naturally a lot of other commands beyond this, though we won’t need them for this book. You could bookmark these four pages for later use, however:

  • Admin commands: Creating user roles, setting passwords, configuring ports, etc.

  • CLI commands: Creating databases, roles, setting passwords for roles, connecting to databases, etc.

  • REPL commands: Mostly shortcuts for a lot of the commands we’ll be using in this book.

  • Various commands about rolling back transactions, declaring savepoints, and so on.

There are also a few places to download packages to highlight your syntax if you like. EdgeDB has these packages available for Atom, Visual Studio Code, Sublime Text, and Vim.

So here’s the City type we just made:

Copy
type City {
  required property name -> str;
  property modern_name -> str;
}

Here are three operators in EdgeDB that have the = sign:

  • := is used to declare,

  • = is used to check equality (not ==),

  • != is the opposite of =.

Let’s try them out with SELECT. SELECT is the main query command in EdgeDB, and you use it to see results based on the input that comes after it.

By the way, keywords in EdgeDB are case insensitive, so SELECT, select and SeLeCT are all the same. But using capital letters is the normal practice for databases so we’ll continue to use them that way.

First we’ll just select a string:

Copy
SELECT 'Jonathan Harker\'s journey begins.';

This returns {'Jonathan Harker\'s journey begins.'}, no surprise there. Did you notice that it’s returned inside a {}? The {} means that it’s a set, and in fact everything in EdgeDB is a set (make sure to remember that). It’s also why EdgeDB doesn’t have null: where you would have null in other languages, EdgeDB just gives you an empty set: {}.

Next we’ll use := to assign a variable:

Copy
SELECT jonathans_name := 'Jonathan Harker';

This just returns what we gave it: {'Jonathan Harker'}. But this time it’s a string that we assigned called jonathans_name that is being returned.

Now let’s do something with this variable. We can use the keyword WITH to use this variable and then compare it to 'Count Dracula':

Copy
WITH jonathans_name := 'Jonathan Harker',
SELECT jonathans_name != 'Count Dracula';

The output is {true}. Of course, you can just write SELECT 'Jonathan Harker' != 'Count Dracula' for the same result. Soon we will actually do something with the variables we assign with :=.

Let’s get back to the schema. Later on we can think about adding time zones and locations for the cities for our imaginary game. But in the meantime, we will add some items to the database using INSERT.

Don’t forget to separate each property by a comma, and finish the INSERT with a semicolon. EdgeDB also prefers two spaces for indentation.

Copy
INSERT City {
  name := 'Munich',
};

INSERT City {
  name := 'Buda-Pesth',
  modern_name := 'Budapest'
};

INSERT City {
  name := 'Bistritz',
  modern_name := 'Bistrița'
};

Note that a comma at the end is optional - you can put it in or leave it out. Here we put a comma at the end sometimes and left it out at other times to show this.

Finally, the Person insert would look like this:

Copy
INSERT Person {
  name := 'Jonathan Harker',
  places_visited := ["Bistritz", "Vienna", "Buda-Pesth"],
};

But hold on a second. That insert won’t link it to any of the City inserts that we already did. Here’s where our schema needs some improvement:

  • We have a Person type and a City type,

  • The Person type has the property places_visited with the names of the cities, but they are just strings in an array. It would be better to link this property to the City type somehow.

So let’s not do that Person insert. We’ll fix the Person type soon by changing array<str> from a property to something called multi link to the City type. This will actually join them together.

But first let’s look a bit closer at what happens when we use INSERT.

As you can see, strs are fine with unicode letters like ț. Even emojis and special characters are just fine: you could even create a City called ‘🤠’ or ‘(╯°□°)╯︵ ┻━┻’ if you wanted to.

EdgeDB also has a byte literal type that gives you the bytes of a string. This is mainly for raw data that humans don’t need to view such when saving to files. They must be characters that are 1 byte long.

You create byte literals by adding a b in front of the string:

Copy
edgedb> 
SELECT b'Bistritz';
{b'Bistritz'}

And because the characters must be 1 byte, only ASCII works for this type. So the name in modern_name as a byte literal will generate an error because of the ț:

Copy
edgedb> 
SELECT b'Bistrița';
error: invalid bytes literal: character 'ț' is unexpected, only ascii chars are allowed in bytes literals

Every time you INSERT an item, EdgeDB gives you a uuid back. That’s the unique number for each item. It will look like this:

{Object {id: d2af670c-f1d6-11ea-a30f-8b40bc5413e0}}

It is also what shows up when you use SELECT to select a type. Just typing SELECT with a type will show you all the uuids for the type. Let’s look at all the cities we have so far:

Copy
SELECT City;

This gives us three items:

{
  Object {id: d2b64e00-f1d6-11ea-a30f-1f161d0b15ae},
  Object {id: d2c023b2-f1d6-11ea-a30f-e3069a47b57e},
  Object {id: d37bc838-f1d6-11ea-a30f-afb031317264},
}

This only tells us that there are three objects of type City. To see inside them, we can add property or link names to the query. This is called describing the shape of the data we want. We’ll select all City types and display their modern_name with this query:

Copy
SELECT City {
  modern_name,
};

Once again, you don’t need the comma after modern_name because it’s at the end of the query.

You will remember that one of our cities (Vienna) doesn’t have anything for modern_name. But it still shows up as an “empty set”, because every value in EdgeDB is a set of elements, even if there’s nothing inside. Here is the result:

{Object {modern_name: {}}, Object {modern_name: 'Budapest'}, Object {modern_name: 'Bistrița'}}

So there is some object with an empty set for modern_name, while the other two have a name. This shows us that EdgeDB doesn’t have null like in some languages: if nothing is there, it will return an empty set.

The first object is a mystery so we’ll add name to the query so we can see that it’s the city of Vienna:

Copy
SELECT City {
  name,
  modern_name
};

This gives the output:

{
  Object {name: 'Vienna', modern_name: {}},
  Object {name: 'Bistritz', modern_name: 'Bistrița'},
  Object {name: 'Buda-Pesth', modern_name: 'Budapest'}
}

If you just want to return a single part of a type without the object structure, you can use . after the type name. For example, SELECT City.modern_name will give this output:

{'Budapest', 'Bistrița'}

This type of expression is called a path expression or a path, because it is the direct path to the values inside. And each . moves on to the next path, if there is another one to follow.

You can also change property names like modern_name to any other name if you want by using := after the name you want. Those names you choose become the variable names that are displayed. For example:

Copy
SELECT City {
  name_in_dracula := .name,
  name_today := .modern_name,
};

This prints:

{
  Object {name_in_dracula: 'Munich', name_today: {}},
  Object {name_in_dracula: 'Buda-Pesth', name_today: 'Budapest'},
  Object {name_in_dracula: 'Bistritz', name_today: 'Bistrița'},
}

This will not change anything inside the schema - it’s just a quick variable name to use in a query.

By the way, .name is short for City.name. You can also write City.name each time (that’s called the fully qualified name), but it’s not required.

So if you can make a quick name_in_dracula property from .name, can we make other things too? Indeed we can. For the moment we’ll just keep it simple but here is one example:

Copy
SELECT City {
  name_in_dracula := .name,
  name_today := .modern_name,
  oh_and_by_the_way := 'This is a city in the book Dracula'
};

And here is the output:

{
  Object {name_in_dracula: 'Munich', name_today: {}, oh_and_by_the_way: 'This is a city in the book Dracula'},
  Object {name_in_dracula: 'Buda-Pesth', name_today: 'Budapest', oh_and_by_the_way: 'This is a city in the book Dracula'},
  Object {name_in_dracula: 'Bistritz', name_today: 'Bistrița', oh_and_by_the_way: 'This is a city in the book Dracula'},
}

Also note that oh_and_by_the_way is of type str even though we didn’t have to tell it. EdgeDB is strongly typed: everything needs a type and it will not try to mix them together. So if you write SELECT 'Jonathan Harker' + 8; it will simply refuse with an error: QueryError: operator '+' cannot be applied to operands of type 'std::str' and 'std::int64'.

On the other hand, it can use “type inference” to guess the type, and that is what it does here: it knows that we are creating a str. We will look at changing types and working with different types soon.

Practice Time
  1. Entering the code below returns an error. Try adding one character to make it return {true}.

    Copy
    WITH my_name = 'Timothy',
    SELECT my_name != 'Benjamin';
    
    Show answer
  2. Try inserting a City called Constantinople, but now known as İstanbul.

    Show answer
  3. Try displaying all the names of the cities in the database. (Hint: you can do it in a single line of code and won’t need {} to do it)

    Show answer
  4. Try selecting all the City types along with their name and modern_name properties, but change .name to say old_name and change modern_name to say name_now.

    Show answer
  5. Will typing SelecT City; produce an error?

    Show answer

Up next: Jonathan Harker arrives in Romania.