Easy EdgeDB · Chapter 8

Dracula takes the boat to England

Multiple InheritancePolymorphism

We are finally away from Castle Dracula. Here is what happens in this chapter:

A boat leaves from the city of Varna in Bulgaria, sailing into the Black Sea. It has a captain, first mate, second mate, cook, and five crew. Inside is Dracula, but they don’t know that he’s there. Every night Dracula leaves his coffin, and every night one of the men disappears. They become afraid but don’t know what is happening or what to do. One of them tells them he saw a strange man walking around the deck, but the others don’t believe him. Finally it’s the last day before the ship reaches the city of Whitby in England, but the captain is alone - all the others have disappeared. The captain knows the truth now. He ties his hands to the wheel so that the ship will go straight even if Dracula finds him. The next day the people in Whitby see a ship hit the beach, and a wolf jumps off and runs onto the shore - it’s Dracula in his wolf form, but they don’t know that. People find the dead captain tied to the wheel with a notebook in his hand and start to read the story.

Meanwhile, Mina and her friend Lucy are in Whitby on vacation…

While Dracula arrives at Whitby, let’s learn about multiple inheritance. We know that you can extend a type on another, and we have done this many times: Person on NPC, Place on City, etc. Multiple inheritance is doing this with more than one type at the same time. We’ll try this with the ship’s crew. The book doesn’t give them any names, so we will give them numbers instead. Most Person types won’t need a number, so we’ll create an abstract type called HasNumber only for types that need a number:

Copy
abstract type HasNumber {
  required property number -> int16;
}

We will also remove required from name for the Person type. Not every Person type will have a name now, and we trust ourselves enough to input a name if there is one. We will of course keep it exclusive.

Now we can use multiple inheritance for the Crewman type. It’s very simple: just add a comma between every type you want to extend.

Copy
type Crewman extending HasNumber, Person {
}

Now that we have this type and don’t need a name, it’s super easy to insert our crewmen thanks to count(). We just do this five times:

Copy
INSERT Crewman {
  number := count(DETACHED Crewman) + 1
};

So if there are no Crewman types, he will get the number 1. The next will get 2, and so on. So after doing this five times, we can SELECT Crewman {number}; to see the result. It gives us:

{
  Object {number: 1},
  Object {number: 2},
  Object {number: 3},
  Object {number: 4},
  Object {number: 5},
}

Next is the Sailor type. The sailors have ranks, so first we will make an enum for that:

Copy
scalar type Rank extending enum<Captain, FirstMate, SecondMate, Cook>;

And then we will make a Sailor type that uses Person and this Rank enum:

Copy
type Sailor extending Person {
  property rank -> Rank;
}

Then we will make a Ship type to hold them all.

Copy
type Ship {
  required property name -> str;
  multi link sailors -> Sailor;
  multi link crew -> Crewman;
}

Now to insert the sailors we just give them each a name and choose a rank from the enum:

Copy
INSERT Sailor {
  name := 'The Captain',
  rank := 'Captain'
};

INSERT Sailor {
  name := 'Petrofsky',
  rank := 'FirstMate'
};

INSERT Sailor {
  name := 'The First Mate',
  rank := 'FirstMate'
};

INSERT Sailor {
  name := 'The Cook',
  rank := 'Cook'
};

Inserting the Ship is easy because right now every Sailor and every Crewman type is part of this ship - we don’t need to FILTER anywhere.

Copy
INSERT Ship {
  name := 'The Demeter',
  sailors := Sailor,
  crew := Crewman
};

Then we can look up the Ship to make sure that the whole crew is there:

Copy
SELECT Ship {
  name,
  sailors: {
    name,
    rank,
  },
  crew: {
    number
  },
};

The result is:

{
  Object {
    name: 'The Demeter',
    sailors: {
      Object {name: 'Petrofsky', rank: FirstMate},
      Object {name: 'The Second Mate', rank: SecondMate},
      Object {name: 'The Cook', rank: Cook},
      Object {name: 'The Captain', rank: Captain},
    },
    crew: {
      Object {number: 1},
      Object {number: 2},
      Object {number: 3},
      Object {number: 4},
      Object {number: 5},
    },
  },
}

On the subject of giving types a number, EdgeDB has a type called sequence that you may find useful. This type is defined as an “auto-incrementing sequence of int64”, so an int64 that starts at 1 and goes up every time you use it. Let’s imagine a Townsperson type for a moment that uses it. Here’s the wrong way to do it:

Copy
type Townsperson extending Person {
  property number -> sequence;
}

This won’t work because each sequence keeps a record of the most recent number, and if every type just uses sequence then they would share it. So the right way to do it is to extend it to another type that you give a name to, and then that type will start from 1. So our Townsperson type would look like this instead:

Copy
scalar type TownspersonNumber extending sequence;

type Townsperson extending Person {
  property number -> TownspersonNumber;
}

The number for a sequence type will continue to increase by 1 even if you delete other items. For example, if you inserted five Townsperson objects, they would have the numbers 1 to 5. Then if you deleted them all and then inserted one more Townsperson, this one would have the number 6 (not 1). So this is another possible option for our Crewman type. It’s very convenient and there is no chance of duplication, but the number increments on its own every time you insert. Well, you could create duplicate numbers using UPDATE and SET (EdgeDB won’t stop you there) but even then it would still keep track of the next number when you do the next insert.

So now we have quite a few types that extend the Person type, many with their own properties. The Crewman type has a property number, while the NPC type has a property called age.

But this gives us a problem if we want to query them all at the same time. They all extend Person, but Person doesn’t have all of their links and properties. So this query won’t work:

Copy
SELECT Person {
  name,
  age,
  number,
};

The error is ERROR: InvalidReferenceError: object type 'default::Person' has no link or property 'age'.

Luckily there is an easy fix for this: we can use IS inside square brackets to specify the type. Here’s how it works:

  • .name: this stays the same, because Person has this property

  • .age: this belongs to the NPC type, so change it to [IS NPC].age

  • .number: this belongs to the Crewman type, so change it to [IS Crewman].number

Now it will work:

Copy
SELECT Person {
  name,
  [IS NPC].age,
  [IS Crewman].number,
};

The output is now quite large, so here’s just a part of it. You’ll notice that types that don’t have a property or a link will return an empty set: {}.

{
  Object {name: 'Woman 1', age: {}, number: {}},
  Object {name: 'The innkeeper', age: 30, number: {}},
  Object {name: 'Mina Murray', age: {}, number: {}},
  Object {name: {}, age: {}, number: 1},
  Object {name: {}, age: {}, number: 2},
   # /snip
}

This is pretty good, but the output doesn’t show us the type for each of them. To refer to an object’s own type in a query in EdgeDB you can use __type__. Calling just __type__ will just give a uuid though, so we need to add {name} to indicate that we want the name of the type. All types have this name field that you can access if you want to show the object type in a query.

Copy
SELECT Person {
  __type__: {
    name      # Name of the type inside module default
  },
  name, # Person.name
  [IS NPC].age,
  [IS Crewman].number,
};

Choosing the five objects from before from the output, it now looks like this:

{
  Object {__type__: Object {name: 'default::MinorVampire'}, name: 'Woman 1', age: {}, number: {}},
  Object {__type__: Object {name: 'default::NPC'}, name: 'The innkeeper', age: 30, number: {}},
  Object {__type__: Object {name: 'default::NPC'}, name: 'Mina Murray', age: {}, number: {}},
  Object {__type__: Object {name: 'default::Crewman'}, name: {}, age: {}, number: 1},
  Object {__type__: Object {name: 'default::Crewman'}, name: {}, age: {}, number: 2},
}

This is officially called a polymorphic query, and is one of the best reasons to use abstract types in your schema.

The official name for a type that gets extended by another type is a supertype (meaning ‘above type’). The types that extend them are their subtypes (‘below types’). Because inheriting a type gives you all of its features, subtype IS supertype will return {true}. And of course, supertype IS subtype returns {false} because supertypes do not inherit the features of their subtypes.

In our schema, that means that SELECT PC IS Person returns {true}, while SELECT Person IS PC will return {true} or {false} depending on whether the object is a PC.

To make a query that will show this, just add a shape query with the computable Person IS PC and EdgeDB will tell you:

Copy
SELECT Person {
    name,
    is_PC := Person IS PC,
};

Now how about the simpler scalar types? We know that EdgeDB is very precise in having different types for integers, floats and so on, but what if you just want to know if a number is an integer for example? Of course this will work, but it’s not very satisfying:

Copy
WITH year := 1887,
SELECT year IS int16 OR year IS int32 OR year IS int64;

Output: {true}.

But fortunately these types all extend from abstract types too, and we can use them. These abstract types all start with any, and are: anytype, anyscalar, anyenum, anytuple, anyint, anyfloat, anyreal. The only one that might make you pause is anyreal: this one means any real number, so both integers and floats, plus the decimal type.

So with that you can change the above input to SELECT 1887 IS anyint and get {true}.

We’ve seen multi link quite a bit already, and you might be wondering if multi can appear in other places too. The answer is yes. A multi property is like any other property, except that it can have more than one value. For example, our Castle type has an array<int16> for the doors property:

Copy
type Castle extending Place {
  property doors -> array<int16>;
}

But it could do something similar like this:

Copy
type Castle extending Place {
  multi property doors -> int16;
}

With that, you would insert using {} instead of square brackets for an array:

Copy
INSERT Castle {
  name := 'Castle Dracula',
  doors := {6, 19, 10},
};

The next question of course is which is best to use: multi property, array, or an object type via a link. The answer is…it depends. But here are some good rules of thumb to help you decide which to choose.

  • multi property vs. arrays:

    How large is the data you are working with? A multi property is more efficient when you have a lot of data, while arrays are slower. But if you have small sets, then arrays are faster than multi property.

    If you want to use indexes and constraints on individual elements, then you should use a multi property. We’ll look at indexes in Chapter 16, but for now just know that they are a way of making lookups faster.

    If order is important, than an array may be better. It’s easier to keep the original order of items in an array.

  • multi property vs. objects

    Here we’ll start with two areas where multi property is better, and then two areas where objects are better.

    First negative for objects: objects are always larger, and here’s why. Remember DESCRIBE TYPE as TEXT? Let’s look at one of our types with that again. Here’s the Castle type:

    {
      'type default::Castle extending default::Place {
        required single link __type__ -> schema::Type {
          readonly := true;
        };
        optional single property doors -> array<std::int16>;
        required single property id -> std::uuid {
          readonly := true;
        };
        optional single property important_places -> array<std::str>;
        optional single property modern_name -> std::str;
        required single property name -> std::str;
    };
    

    You’ll remember seeing the readonly := true types, which are created for each object type you make. The __type__ link and id property together always make up 32 bytes.

    The second negative for objects is similar: underneath, they are more work for the computer. EdgeDB runs on top of PostgreSQL, and a multi link to an object needs an extra “join” (a link table + object table), but a multi property only has one. Also, a “backward link” or “reverse link” (you’ll see those in Chapter 14) takes more work as well.

    Okay, now here are two positives for objects in comparison.

    Do you have a lot of duplication in property values? If so, then using constraint exclusive on an object type is the more efficient way to do it.

    Objects are easier to migrate if you need to have more than one value with each.

So hopefully that explanation should help. You can see that you have a lot of choice, so remembering the points above should help you make a decision. Most of the time, you’ll probably have a sense for which one you want.

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

Practice Time
  1. How would you select all the Place types and their names, plus the door property if it’s a Castle?

    Show answer
  2. How would you select Place types with city_name for name if it’s a City and country_name for name if it’s a Country?

    Show answer
  3. How would you do the same but only showing the results of City and Country types?

    Show answer
  4. How would you display all the Person types that don’t have lovers, with their names and their type names?

    Show answer
  5. What needs to be fixed in this query? Hint: two things definitely need to be fixed, while one more should probably be changed to make it more readable.

    Copy
    SELECT Place {
      __type__,
      name
      [IS Castle]doors
    };
    
    Show answer

Up next: Time to meet Dr. Seward, Arthur Holmwood, and Quincey Morris…and the strange Renfield.