Easy EdgeDB · Chapter 11

What’s wrong with Lucy?

Writing FunctionsMultiplicationCoalescing

Dr. Van Helsing thinks that Lucy is being visited by a vampire. He doesn’t tell the others yet because they won’t believe him, but says they should close the windows and put garlic everywhere. They are confused, but Dr. Seward tells them to listen: Dr. Van Helsing is the smartest person he knows. It works, and Lucy gets better. But one night Lucy’s mother walks into the room and thinks: “This place smells terrible! I’ll open the windows for some fresh air.” The next day Lucy wakes up pale and sick again. Every time someone makes a mistake like this Dracula gets in her room, and every time the men give Lucy their blood to help her get better. Meanwhile, Renfield continues to try to eat living things and Dr. Seward can’t understand him. Then one day he didn’t want to talk, only saying: “I don’t want to talk to you: you don’t count now; the Master is at hand.”

We are starting to see more and more events in the book with various characters. Some events have the three men and Dr. Van Helsing together, others have just Lucy and Dracula. Previous events had Jonathan Harker and Dracula, Jonathan Harker and the three women, and so on. In our game, we could use a sort of Event type to group everything together: the people, the time, the place, and so on.

This Event type is a bit long, but it would be the main type for our events in the game so it needs to be detailed. We can put it together like this:

type Event {
  required property description -> str;
  required property start_time -> cal::local_datetime;
  required property end_time -> cal::local_datetime;
  required multi link place -> Place;
  required multi link people -> Person;
  property exact_location -> tuple<float64, float64>;
  property east -> bool;
  property url := 'https://geohack.toolforge.org/geohack.php?params=' ++ <str>.exact_location.0 ++ '_N_' 
  ++ <str>.exact_location.1 ++ '_' ++ ('E' if .east else 'W');

You can see that most of the properties are required, because an Event type is not useful if it doesn’t have all the information we need. It will always need a description, a time, place, and people participating. The interesting part is the url property: it’s a computed property that gives us an exact url for the location if we want. This one is not required because not every event in the book is in a perfectly known location.

The url that we are generating needs to know whether a location is east or west of Greenwich, and also whether they are north or south. Here is the url for Bistritz, for example:


Luckily for us, the events in the book all take place in the north part of the planet. So N is always going to be there. But sometimes they are east of Greenwich and sometimes west. To decide between east and west, we can use a simple bool. Then in the url property we put all the properties together to create a link, and finish it off with ‘E’ if east is true, and ‘W’ otherwise.

(Of course, if we were receiving longitudes as simple positive and negative numbers (+ for east, - for west) then east could be a computed property: property east := true if exact_location.0 > 0 else false. But for this schema we’ll imagine that we are getting numbers from somewhere with this sort of format: [50.6, 70.1, true])

Let’s insert one of the events in this chapter. It takes place on the night of September 11th when Dr. Van Helsing is trying to help Lucy. You can see that the description property is just a string that we write to make it easy to search later on. It can be as long or as short as we like, and we could even just paste in parts of the book.

insert Event {
  description := "Dr. Seward gives Lucy garlic flowers to help her sleep. She falls asleep and the others leave the room.",
  start_time := cal::to_local_datetime(1887, 9, 11, 18, 0, 0),
  end_time := cal::to_local_datetime(1887, 9, 11, 23, 0, 0),
  place := (select Place filter .name = 'Whitby'),
  people := (select Person filter .name ilike {'%helsing%', '%westenra%', '%seward%'}),
  exact_location := (54.4858, 0.6206),
  east := false

With all this information we can now find events by description, character, location, and so on.

Now let’s do a query for all events with the word garlic flowers in them:

select Event {
  place: {
    __type__: {
  people: {
} filter .description ilike '%garlic flowers%';

It generates a nice output that shows us everything about the event:

  default::Event {
    description: 'Dr. Seward gives Lucy garlic flowers to help her sleep. She falls asleep and the others leave the room.',
    start_time: <cal::local_datetime>'1887-09-11T18:00:00',
    end_time: <cal::local_datetime>'1887-09-11T23:00:00',
    place: {
      default::City {__type__: schema::ObjectType {name: 'default::City'}, name: 'Whitby'},
    people: {
      default::NPC {name: 'Lucy Westenra'},
      default::NPC {name: 'John Seward'},
      default::NPC {name: 'Abraham Van Helsing'},
    exact_location: (54.4858, 0.6206),
    url: 'https://geohack.toolforge.org/geohack.php?params=54.4858_N_0.6206_W',

The url works nicely too. Here it is: https://geohack.toolforge.org/geohack.php?params=54.4858_N_0.6206_W Clicking on it takes you directly to the city of Whitby.

We saw that Renfield is quite strong: he has a strength of 10, compared to Jonathan’s 5.

We could use this to experiment with making functions now. Because EdgeQL is strongly typed, you have to indicate both the input type and the return type in the signature. A function that takes an int16 and gives a float64 for example would have this signature:

function does_something(input: int16) -> float64

The -> skinny arrow is used to show the return value.

For the body of the function we do the following:

  • Write using and then follow it up with () brackets,

  • Write the function inside it,

  • Finish with a semicolon.

Here’s a very simple function that takes a number and returns a string from it:

function make_string(input: int64) -> str
  using (<str>input);

That’s all there is to it!

Now let’s write a function where we have two characters fight. We will make it as simple as possible: the character with more strength wins, and if their strength is the same then the second player wins.

function fight(one: Person, two: Person) -> str
  using (
    one.name ++ ' wins!'
    if one.strength > two.strength
    else two.name ++ ' wins!'

The function looks good, but when you try to create it, you’ll get an error:

InvalidFunctionDefinitionError: return cardinality mismatch in function
  declared to return exactly one value

This happens because name and strength are not required on our Person type. If we pass this function at least one Person without a value for one of these properties, the function will return an empty set. (More on that in the next chapter.) EdgeDB doesn’t like this because we’ve told it in the function definition that the function will return a string.

We could go back and require the name and strength properties. We’d need to make sure all of our Person objects have values for each of them. That’s a lot of trouble, and it’s not something we’re ready to do right now.

The easiest way to fix our function would be to provide some sort of fallback for the properties that might not be set. If one doesn’t have a name, we could just refer to them as Fighter 1. If someone doesn’t have a strength, we could just default their strength to 0.

To do that we can use the coalescing operator, which is written ??. It evaluates to whatever is on the left if that’s not empty. Otherwise, it evaluates to whatever is on the right.

Here is a quick example:

select <str>{} ?? 'Count Dracula is now in Whitby';

The empty set is on the left, but since it is the empty set, it doesn’t get the nod from the coalescing operator. Instead, this query will produce the string to the right of the coalescing operator: {'Count Dracula is now in Whitby'}

If neither side of the operator is the empty set, the coalescing operator will produce whatever is on the left. If both sides are the empty set, it will produce the empty set.

Here’s how we can use the coalescing operator to fix our function:

function fight(one: Person, two: Person) -> str
  using (
    (one.name ?? 'Fighter 1') ++ ' wins!'
    if (one.strength ?? 0) > (two.strength ?? 0)
    else (two.name ?? 'Fighter 2') ++ ' wins!'

Now, EdgeDB has fallbacks in the event one of those values is an empty set. If one.name is the empty set, we get 'Fighter 1'. If one of the strength properties is the empty set, we get 0. If two.name is the empty set, we get 'Fighter 2. This ensures that the function can always return the string response we promised.

So far only Jonathan and Renfield have the property strength, so let’s put them up against each other in this new fight() function:

  renfield := (select Person filter .name = 'Renfield'),
  jonathan := (select Person filter .name = 'Jonathan Harker')
select (
  fight(jonathan, renfield)

It prints what we wanted to see: {'Renfield wins!'}

It might also be a good idea to add assert_single() when doing a filter for this function. Because EdgeDB returns sets, if it gets multiple results then it will use the function against each one for each possible combination. The way EdgeDB handles this is through Cartesian multiplication, so let’s learn about that now.

Cartesian multiplication sounds intimidating but it really just means “join every item in one set to every item in the other set”. It’s easiest to understand when viewed as an illustration, which fortunately Wikipedia has already made for us. When you multiply sets in EdgeDB you are given the Cartesian product, which looks like this:

A chart displaying the Cartesian product of 1, 2, 3 multiplied by x, y, and z

Source: user quartl on Wikipedia

This means that if we do a select on Person for our fight() function, it will run the function following this formula:

  • {the number of items in the first set} * {the number of items in the second set}

So if there are two in the first set, and three in the second, it will run the function six times.

To demonstrate, let’s put three objects in for each side of our function. We’ll be testing our fight function, so we’ll just give all the characters strength value 5 if they don’t already have some other value:

update Person filter not exists .strength
set {
  strength := 5

We’ll also make the output a little more clear:

  first_group := (select Person filter .name in {'Jonathan Harker', 'Count Dracula', 'Arthur Holmwood'}),
  second_group := (select Person filter .name in {'Renfield', 'Mina Murray', 'The innkeeper'}),
select (
  first_group.name ++ ' fights against ' ++ second_group.name ++ '. ' ++ fight(first_group, second_group)

Here is the output. It’s a total of nine fights, where each person in Set 1 fights once against each person in Set 2.

  'Jonathan Harker fights against Renfield. Renfield wins!',
  'Jonathan Harker fights against The innkeeper. The innkeeper wins!',
  'Jonathan Harker fights against Mina Murray. Mina Murray wins!',
  'Arthur Holmwood fights against Renfield. Renfield wins!',
  'Arthur Holmwood fights against The innkeeper. The innkeeper wins!',
  'Arthur Holmwood fights against Mina Murray. Mina Murray wins!',
  'Count Dracula fights against Renfield. Renfield wins!',
  'Count Dracula fights against The innkeeper. The innkeeper wins!',
  'Count Dracula fights against Mina Murray. Mina Murray wins!',

And if you take out the filter and just write select Person for the function, you will get well over 100 results. EdgeDB by default will only show the first 100, displaying this after showing you 100 results:

... (further results hidden `\set limit 100`)

Here is all our code so far up to Chapter 11.

Practice Time
  1. How would you write a function called lucy() that just returns all the NPC types matching the name ‘Lucy Westenra’?

    Show answer
  2. How would you write a function that takes two strings and returns Person objects with names that match each string?

    Hint: try using set of Person as the return type.

    Show answer
  3. What will the output of this be?

    select {'Jonathan', 'Arthur'} ++ {' loves '} ++ {'Mina', 'Lucy'} ++ {' but '} ++ {'Dracula', 'The inkeeper'} ++ {' doesn\'t love '} ++ {'Mina', 'Jonathan'};
    Show answer
  4. How would you make a function that tells you how many times larger one city is than another?

    Show answer
  5. Will select (City.population + City.population) and select ((select City.population) + (select City.population)) produce different results?

    Show answer

Up next: Lucy one night: “What’s that flapping against the window? Sounds like a bat or something…”