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 in Romania and is now written Bistrița (note the ț - it’s Bistrița, not Bistrita), while 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", "Munich", "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. Munich is still Munich, 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.

Easy EdgeDB’s instructions on migrations were written in 2020 and are somewhat out of date - they work, but are less convenient than the current options. As of May 2023 the preferred (and easiest) way to do a migration is by using the CLI commands at https://www.edgedb.com/docs/intro/quickstart. To start a project, just type ‘edgedb project init’. And every time you change your schema, just type ‘edgedb migration create’ followed by ‘edgedb migrate’. Easy EdgeDB’s chapters will be updated with the easier migration tools very soon!

Another recent addition to EdgeDB is the interactive UI. To use it in an initialized project, just type ‘edgedb ui’ and it will open up in your browser. The UI is a great way to view your schema and play around with your data while you are still learning the EdgeQL query language that this book teaches you.

And now back to the book…

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 “project” that makes it easier to keep track of the schema and deal with migrations. Then we just open a console to our database by running edgedb, which will connect us to the default database called “edgedb”. We’ll use that a lot for experimenting.

Sometimes it’s useful to create a whole new database to try something out. You can do that with the create database keyword and our name for it:

Copy
create database dracula;

Then we type \c dracula to connect to it. And you can type \c edgedb to get back to the default one.

Lastly, we need to do a migration. This will give the database the structure we need to start interacting with it.

  • 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.

Putting all of that together we get:

Copy
start migration to {
  module default {
    type Person {
      required property name -> str;
      property places_visited -> array<str>;
    }

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

populate migration;
commit migration;

There are 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;
}

The select keyword is the main query command in EdgeDB, and you use it to see results based on the input that comes after it. Keywords in EdgeDB are case insensitive, so select, SELECT and SeLeCT are all the same.

Let’s give select a try with something really easy: just selecting a string.

Copy
select 'Jonathan Harker begins his journey.';

This returns {'Jonathan Harker begins his journey.'}, 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: {}. The advantage here is that sets always behave the same way even when they are empty, instead of all the unwelcome surprises that come with using null.

For the next select queries, we will use some more operators that use the = sign:

  • := is used to declare,

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

  • != is the opposite of =.

Let’s 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 select to use this variable and then compare it to 'Count Dracula':

Copy
select 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 do more complex operations with the variables that 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 after the last item 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", "Munich", "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 a 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, strings (str) 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
  ┌─ query:1:8
  │
1 │ select b'Bistrița';
  │        ^ error

Every time you insert an item, EdgeDB gives you a uuid back. UUID stands for Universally Unique IDentifier, and is used widely to make identifiers that won’t be used anywhere else. It will look like this:

{default::Person {id: 462b29ea-ff3d-11eb-aeb7-b3cf3ba28fb9}}

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:

{
  default::City {id: 4ba1074e-ff3f-11eb-aeb7-cf15feb714ef},
  default::City {id: 4bab8188-ff3f-11eb-aeb7-f7b506bd047e},
  default::City {id: 4bacf860-ff3f-11eb-aeb7-97025b4d95af},
}

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 (Munich) 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:

{
  default::City {modern_name: {}},
  default::City {modern_name: 'Budapest'},
  default::City {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 Munich:

Copy
select City {
  name,
  modern_name
};

This gives the output:

{
  default::City {name: 'Munich', modern_name: {}},
  default::City {name: 'Buda-Pesth', modern_name: 'Budapest'},
  default::City {name: 'Bistritz', modern_name: 'Bistrița'},
}

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:

{
  default::City {name_in_dracula: 'Munich', name_today: {}},
  default::City {name_in_dracula: 'Buda-Pesth', name_today: 'Budapest'},
  default::City {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:

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

This brings up an interesting discussion about type safety. EdgeDB is strongly typed, meaning that everything needs a type and it will not try to mix different types together. So if you write select 'Jonathan Harker' + 8; it will simply refuse with an error: operator '+' cannot be applied to operands of type 'std::str' and 'std::int64'. But we didn't declare a type for oh_and_by_the_way, so how did EdgeDB know that it was a str`?

EdgeDB uses what is known as “type inference” to guess the type, meaning that it can usually figure out the type itself. That is what happens here: EdgeDB knows that we are creating a str because we enclosed it in quotes. In other words, the type is still a concrete str even if we didn’t specify that it was.

In fact, we can prove that these types are concrete right away: let’s just ask EdgeDB.

Copy
select 'Jonathan Harker' is str;

This returns {true}. Let’s try another:

Copy
select 8 is int64;

This returns {true}. Let’s try one more!

Copy
select 8 is int32;

This time it returns {false}. As you can see, EdgeDB is selecting a concrete type every time even if we don’t specify a type.

There is a way to change one type to another that we will learn in the next chapter.

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.