Search
ctrl/
Ask AI
Light
Dark
System
Easy EdgeDB · Chapter 14

A ray of hope

Type AnnotationsBacklinks

Finally there is some good news: Jonathan Harker is alive. It seems that he managed to escape Castle Dracula, after which he found his way to Buda-Pesth (Budapest) in August and then to a hospital. The staff at the hospital are worried about Jonathan’s mental health and send Mina a letter in which they say that he had “had some fearful shock and continues to talk about wolves and poison and blood, of ghosts and demons.”

Poor Jonathan! Sitting in the hospital he begins to wonder if he has just gone insane. Was Count Dracula real? Were the vampire women real too? Or did he just imagine the whole thing? How can Mina marry a man who is slowly going insane?

Mina takes a train to the hospital where Jonathan is recovering, after which they take a train back to England to the city of Exeter where they get married. Mina sends Lucy a letter from Exeter about the good news…but it arrives too late and Lucy never opens it.

Meanwhile, Van Helsing continues to contact his associates in universities around Europe to search for information on vampires and their activities. The men visit the graveyard as planned and see vampire Lucy walking around. When Arthur sees Lucy he finally believes Van Helsing, and so do the rest of the men. They now know that vampires are real, and manage to destroy her. Arthur is sad but also happy to see that Lucy is no longer forced to be a vampire and can now die in peace.

Looks like we have a new city called Exeter, which is easy to add:

Copy
insert City {
  name := 'Exeter',
  population := 40000
};

That’s the population of Exeter at the time of the book (it has 130,000 people today), and it doesn’t have a modern_name that is different from the one in the book.

We can also update the city of Buda-Pesth to add the name of the hospital where Jonathan Harker was staying. In addition, one of the universities that Van Helsing contacted for information is in the same city. Let’s add them both:

Copy
update City filter .name = 'Buda-Pesth' 
  set { important_places := [
    'Hospital of St. Joseph and Ste. Mary',
    'Buda-Pesth University'
    ] 
  };

Now that we know how to do introspection queries, we can start to give annotations to our types. An annotation is a string inside the type definition that gives us information about it when using an introspect query or when we put __type__ into a query on an object type. By default, annotations can use the names title, description, or deprecated.

Let’s imagine that in our game a City needs at least 50 buildings, and we want the other developers to know this. Let’s use an annotation description for this:

Copy
type City extending Place {
  annotation description := 'A place with 50 or more buildings. Anything else is an OtherPlace';
  population: int64;
}

After migrating our schema, we can now do an introspect query on it. We know how to do this from the last chapter - just add : {name} everywhere to get the inner details. Ready!

Copy
select (introspect City) {
  annotations: {name}
  name,
  properties: {name},
};

Uh oh, not quite. The annotations part of the introspect query just says std::description:

{
  schema::ObjectType {
    annotations: {schema::Annotation {name: 'std::description'}},
    name: 'default::City',
    properties: {
      schema::Property {name: 'name'},
      schema::Property {name: 'modern_name'},
      schema::Property {name: 'important_places'},
      schema::Property {name: 'id'},
      schema::Property {name: 'population'},
    },
  },
}

Let’s do an introspect query on City again, but this time using the splat operator on the annotations to see what is inside.

Copy
select (introspect City) { annotations: {*}};

There it is!

{
  schema::ObjectType {
    annotations: {
      schema::Annotation {
        id: 6478af55-27fe-11ee-a636-8f86ec27644b,
        name: 'std::description',
        internal: false,
        builtin: true,
        computed_fields: [],
        inheritable: false,
        @is_owned: true,
        @owned: true,
        @value: 'A place with 50 or more buildings. Anything else is an OtherPlace',
      },
    },
  },
}

Ah, of course: the annotations: {name} part returns the name of the type, which is std::description. In other words, it’s a link, and the target of a link just tells us the kind of annotation that gets used. But we’re looking for the value inside it. And we can see from the @ in the output above that the value of an annotation is a link property.

Let’s try the query one more time:

Copy
select (introspect City) {
  annotations: {
  name,
  @value
},
  name,
  properties: {name},
};

And now the actual annotation shows up in the output.

{
  schema::ObjectType {
    annotations: {
      schema::Annotation {
        name: 'std::description',
        @value: 'A place with 50 or more buildings. Anything else is an OtherPlace',
      },
    },
    name: 'default::City',
    properties: {
      schema::Property {name: 'name'},
      schema::Property {name: 'modern_name'},
      schema::Property {name: 'important_places'},
      schema::Property {name: 'id'},
      schema::Property {name: 'population'},
    },
  },
}

What if we want an annotation with a different name besides title, description, or deprecated? Easy: just declare a new annotation by typing abstract annotation inside the schema and give it a name. We want to add a warning for other developers to read so that’s what we’ll call it:

Copy
abstract annotation warning;

Maybe it is important to use Castle instead of OtherPlace for not just castles, but castle towns too. Thanks to the new abstract annotation, now OtherPlace gives that information along with the other annotation. Here are the two annotations to add to OtherPlace:

Copy
type OtherPlace extending Place {
  annotation description := 'A place with under 50 buildings - hamlets, small villages, etc.';
  annotation warning := 'Castles and castle towns do not count! Use the Castle type for that';
}

Now let’s migrate the schema again and do an introspect query on just its name and annotations:

Copy
select (introspect OtherPlace) {
  name,
  annotations: {name, @value}
};

And here it is:

{
  schema::ObjectType {
    name: 'default::OtherPlace',
    annotations: {
      schema::Annotation {
        name: 'std::description',
        @value: 'A place with under 50 buildings - hamlets, small villages, etc.',
      },
      schema::Annotation {
        name: 'default::warning',
        @value: 'Castles and castle towns do not count! Use the Castle type for that',
      },
    },
  },
}

Every game needs to be tested before it can be sold, and it’s nice to have different possible modes when testing a game. Any game testers should be able to experience the game in the same way that a regular player would, but another mode with extra information would be helpful too.

Another global type could help here. We’ve had a global Time object in our database for some time now, which so far is our only global type. But globals can be scalar types too.

A global scalar isn’t an object though, so changing its value is a bit different: instead, we use the set and unset keywords to work with it.

To try using a global scalar we can add an enum called Mode, and give it two values: Info or Debug. Info will be the default, while Debug will be the mode that provides extra information for the testers. After this we can make a global called tester_mode:

Copy
scalar type Mode extending enum<Info, Debug>;

required global tester_mode: Mode {
    default := Mode.Info;
  }

A required global always needs a default value, which makes sense: a global is available across the entire database and is required so it must be present. The only way to ensure this is to add a default value. Fortunately, the EdgeDB compiler won’t let a schema migration happen if we forget this. The error message would look like this:

error: required globals must have a default
  ┌─ c:\easy-edgedb\dbschema\default.esdl:9:3
  │
9 │   required global tester_mode: Mode;
  │   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error

edgedb error: cannot proceed until .esdl files are fixed

With the migration done, let’s make sure that the global value is there:

Copy
select global tester_mode;

The output is simple: just {Info}.

Changing a global scalar is easy too: just use the set keyword.

Copy
set global tester_mode := Mode.Debug;

The output here is simple too, just a message informing us that the value was successfully set:

OK: SET GLOBAL

The opposite of the set keyword is reset, which resets a global to its default value. In our case the default value is Mode.Info, but if we hadn’t specified that tester_mode is required then the default value would have been {}, an empty set.

Pretty easy! The output below shows the sort of feedback you will see in the REPL when setting and resetting a global scalar value.

db> select global tester_mode;
{Info}
db> set global tester_mode := Mode.Debug;
OK: SET GLOBAL
db> select global tester_mode;
{Debug}
db> reset global tester_mode;
OK: RESET GLOBAL

And with this global value in place, we can now do queries that match on the tester_mode enum. Here is an example of a query that a tester using the PC named Emil Sinclair might use. During regular Info mode the query will only show the character’s own info, but during Debug mode it will also show info on all the NPC objects as well. In a more complex schema we can imagine that this could be used to show a tester the health, location and so on of all the NPCs in a game, which could then be used to show them on a map or in a separate chart on the screen that is only visible during debug mode.

Copy
# Select all NPC objects in Debug mode
with info := NPC if global tester_mode = Mode.Debug 
             else <NPC>{},
  select PC {
    name,
    class,
    strength,
    locations := .places_visited.name,
    npc_info := info { 
      name, 
      strength
    }
} filter .name = 'Emil Sinclair';

The output is pretty short during Info mode, which only provides the necessary info to play the game in the same way as every other player.

{
  default::PC {
    name: 'Emil Sinclair',
    class: Mystic,
    strength: 2,
    locations: {},
    npc_info: {},
  },
}

But if you use set global tester_mode := Mode.Debug; then all of a sudden the same query will display all of the extra info!

{
  default::PC {
    name: 'Emil Sinclair',
    class: Mystic,
    strength: 2,
    locations: {},
    npc_info: {
      default::NPC {name: 'Jonathan Harker', strength: 5},
      default::NPC {name: 'Renfield', strength: 10},
      default::NPC {name: 'The innkeeper', strength: 1},
      default::NPC {name: 'Mina Murray', strength: 2},
      default::NPC {name: 'Quincey Morris', strength: 4},
      default::NPC {name: 'Arthur Holmwood', strength: 4},
      default::NPC {name: 'John Seward', strength: 3},
      default::NPC {name: 'Abraham Van Helsing', strength: 1},
      default::NPC {name: 'Lucy Westenra', strength: 0},
    },
  },
}

You could imagine some other combinations of global modes such as a “God Mode” that also makes the character invincible - because it’s annoying to try to debug test a game when other PCs keep killing your character without knowing that you are just there to test the game mechanics.

Practice Time
  1. How would you create a global str that tells you whether vampires are currently asleep or awake?

    Show answer
  2. Using a computed backlink, how would you display 1) all the Place objects (plus their names) that have an o in the name and 2) the names of the people that visited them?

    Show answer
  3. Using a computed backlink, how would you display all the Person objects that will later become MinorVampires?

    Hint: Remember, MinorVampire has a link back to the vampire’s former self.

    Show answer
  4. How would you give the MinorVampire type an annotation called note that says 'first_appearance for MinorVampire should always match last_appearance for its matching NPC type'?

    Show answer
  5. How would you see this note annotation for MinorVampire in a query?

    Show answer

Up next: Time to get revenge.