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:

Far away from Castle Dracula, 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. Dracula is also on the ship inside a coffin, but the crew 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 crewman tells the others that he saw a strange man walking around the deck, but the others don’t believe him.

The days go by and more and more sailors disappear…

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 disguised as a wolf. 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 means to do this with more than one type at the same time.

We’ll try some multiple inheritance 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 number: int16;
}

Now let’s put the Crewman type together, which will use multiple inheritance. Multiple inheritance is very simple: just add a comma between every type you want to extend.

Copy
type Crewman extending HasNumber, Person;

However, here we have a problem: we never learn the names of the crewmen in the book, but the property name on Person is required. We know that every Crewman object will have a number, so it would be nice to use this to give them a name like “Crewman 1”, “Crewman 2”, and so on. How can we make this happen?

We can make this happen by giving name a default for the Crewman type. To give a default value, just add {} after the parameter and then give an expression for the default value. So far that gives us a Crewman type that looks like the code below. However, a migration won’t quite work yet. Can you guess why?

Copy
type Crewman extending HasNumber, Person {
  name: str {
    default := '' ++ <str>.number;
  }
}

Fortunately, the error message tells us exactly what to do.

error: property 'name' of object type 'default::Crewman' must be declared using the `overloaded` keyword because it is defined in the following ancestor(s): default::Person
  ┌─ c:\rust\easy-edgedb\dbschema\default.esdl:6:5
  │
6 │ ╭     name: str {
7 │ │       default := '' ++ <str>.number;
8 │ │     }
  │ ╰─────^ error

In other words, Person is where the definition for name is, and this definition doesn’t include any information about a default value. We still want to use name, but in a slightly different way. That’s where the word overloaded comes in. Adding overloaded will keep the other rules for name (that it must be a str, and that it must be exclusive) and will add our new specification that it will have a default value for the Crewman type. So just add overloaded and our work is done!

Copy
type Crewman extending HasNumber, Person {
  overloaded name: str {
    default := 'Crewman ' ++ <str>.number;
  }
}

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. The sailors in the book all have their own names, so we don’t need to overload their name keyword.

Copy
type Sailor extending Person {
  rank: Rank;
}

Then we will make a Ship type to hold them all. As we saw in this chapter, a Ship object can move on its own even if all of its sailors and crewmen are dead, so we won’t make its sailors or crew required.

Copy
type Ship {
  required name: str;
  multi sailors: Sailor;
  multi crew: Crewman;
}

With all those changes done, let’s do a migration.

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

Copy
with next_number := count(Crewman) + 1,
  insert Crewman {
  number := next_number
};

So if there are no Crewman types, he will get the number 1. The next will get 2, and so on. After the five inserts we can select Crewman {name, number}; to see the result. It gives us:

{
  default::Crewman {name: 'Crewman 1', number: 1},
  default::Crewman {name: 'Crewman 2', number: 2},
  default::Crewman {name: 'Crewman 3', number: 3},
  default::Crewman {name: 'Crewman 4', number: 4},
  default::Crewman {name: 'Crewman 5', number: 5},
}

Now to insert the sailors we just give them each a name and choose a rank from the enum. Let’s mix up the enums between str and proper enum input to remind ourselves that EdgeDB will accept either one.

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

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

insert Sailor {
  name := 'The Second Mate',
  rank := Rank.SecondMate
};

insert Sailor {
  name := 'The Cook',
  rank := Rank.Cook
};

Finally we have to insert a Ship to hold them all. This insert 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: {
    name,
    number
  },
};

The result is:

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

We now 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 since the Person type itself doesn’t have the properties age or number, we can’t just make a Person shape that includes them:

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

The error is:

error: InvalidReferenceError: object type 'default::Person'
has no link or property 'age'

It looks like the only property of the three that we can put in this query is name. That feels pretty limiting!

Copy
select Person {
  name
};

So is there a way to include age if the type is an NPC and number if the type is a Crewman? Yes, there is! We can use the is keyword inside square brackets to specify the type. Here’s how it works in our query on Person objects:

  • .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: {}. So the Crewman objects have an age: {} while other objects have a number: {}.

{
  # ... /snip
  default::Crewman {name: 'Crewman 4', age: {}, number: 4},
  default::Crewman {name: 'Crewman 5', age: {}, number: 5},
  default::PC {name: 'Emil Sinclair', age: {}, number: {}},
  default::NPC {name: 'The innkeeper', age: 30, number: {}},
  default::NPC {name: 'Mina Murray', age: {}, number: {}},
  default::NPC {name: 'Jonathan Harker', age: {}, number: {}},
}

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

Let’s do a quick experiment with the same query as above, except with the <json> cast. What differences do you notice?

Copy
select <json>Person {
  name,
  [is NPC].age,
  [is Crewman].number,
};

Here is part of the output:

{"age": null, "name": "Emil Sinclair", "number": null}
{"age": null, "name": "Vampire Woman 1", "number": null}
{"age": null, "name": "The Captain", "number": null}
{"age": null, "name": null, "number": 1}

The type information is all gone! This makes sense because a JSON object is just a bunch of keys and values, and with a concrete query like PC this would be no problem. But this query includes various object types extending Person and it’s hard to tell which type is which. Fortunately, we can put the type information by adding __type__ which is used to refer to an object’s own type:

Copy
select <json>Person {
  name,
  [is NPC].age,
  [is Crewman].number,
  __type__
};

The result is close to what we want, but not quite. Take a look at two of the results:

{
  "age": null,
  "name": "Emil Sinclair",
  "number": null,
  "__type__": {"id": "4a007f07-f91f-11ed-8096-7bf54ff85912"}
}
{
  "age": null,
  "name": "The Captain",
  "number": null,
  "__type__": {"id": "48b9bb2f-faaa-11ed-966c-6fc3482a7805"}
}

The id property shows us that they are two different types, but the type name isn’t readable. To fix this, we can add the name property after __type__ to display this instead of the id.

Copy
select <json>Person {
  name,
  [is NPC].age,
  [is Crewman].number,
  __type__: {
    name
  }
};

And now the two objects from out previous output have human-readble names.

{"age": null, "name": "Emil Sinclair", "number": null, "__type__": {"name": "default::PC"}}
{"age": null, "name": "The Captain", "number": null, "__type__": {"name": "default::Sailor"}}

So what is __type__, exactly? Well, it’s a link that all objects have that are used to describe it. You can see this if you type describe type PC as text; (or with any other object in the schema). Inside the description returned you’ll see this:

required single link __type__: schema::ObjectType {
    readonly := true;
};

Interesting! So it’s just an object that can be queried like any other. Let’s give it a try with PC and the splat operator to see everything inside:

Copy
select PC.__type__ {*};

This will show all of the properties for ObjectType, including the name:

{
  schema::ObjectType {
    id: c7c1983a-268c-11ee-8c82-c79bbe432a02,
    name: 'default::PC',
    internal: false,
    builtin: false,
    computed_fields: [],
    final: false,
    is_final: false,
    abstract: false,
    is_abstract: false,
    inherited_fields: [],
    from_alias: false,
    is_from_alias: false,
    expr: {},
    compound_type: false,
    is_compound_type: false,
  },
}

So that can be pretty useful.

But if you really want to understand the inner workings of EdgeDB, try the same query with the double splat operator:

select PC.__type__ {**};

This will return pages and pages of information. You’ll see a link called pointers that points to just about everything: a link to __type__, a link to strength, a link to is_single and that it is a computable made from the expression not exists .lover…and so on and so on. If you want to get a good feel for how EdgeDB works on the inside, definitely grab a cup of coffee and give this query a try!

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’). You can visualize it like this:

  • abstract type Person (supertype, above)

  • ↳ type PC (subtype, under)

  • ↳ type NPC (subtype, under)

Because inheriting a type gives you all of its features, a query on objects with subtype is supertype will always return {true}. In our schema a PC object is always a Person, and an NPC object is always a Person.

Conversely, supertype is subtype will return {true} or {false} depending on the concrete type of each object returned. A Person object might be a PC object, and it might be an NPC object.

To make a query that will check this, just add a shape query with the computed property Person is PC and EdgeDB will tell you:

Copy
select Person {
    name,
    is_PC := Person is PC,
};

The output will look like this:

{"name": "Emil Sinclair", "is_PC": true}
{"name": "Vampire Woman 1", "is_PC": false}
{"name": "Vampire Woman 2", "is_PC": false}
# ... and so on

Now how about the simpler scalar types? It’s nice that EdgeDB is strict about type safety and has different types for integers, floats and so on, but what if you just want to know if a number is an integer or a float? We could check to see if an integer is one of any integer types, but this makes for a pretty awkward query:

Copy
with year := 1893,
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 with an unclear name 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 1893 is anyint; and get {true}.

Practice Time
  1. How would you select all the Place types and their names, plus the doors 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.

We use ChatGPT with additional context from our documentation to answer your questions. Not all answers will be accurate. Please join our Discord if you need more help.