Adding a required link

This example shows how to setup a required link. We’ll use a character in an adventure game as the type of data we will evolve.

Let’s start with this schema:

  1. type Character {
  2. required property name -> str;
  3. }

We edit the schema file and perform our first migration:

  1. $
  1. edgedb migration create
  1. did you create object type 'default::Character'? [y,n,l,c,b,s,q,?]
  2. > y
  3. Created ./dbschema/migrations/00001.edgeql, id:
  4. m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a
  1. $
  1. edgedb migrate
  1. Applied m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a
  2. (00001.edgeql)

This time around let’s practice performing a data migration and set up our character data. For this purpose we can create an empty migration and fill it out as we like:

  1. $
  1. edgedb migration create --allow-empty
  1. Created ./dbschema/migrations/00002.edgeql, id:
  2. m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq

We edit the 00002.edgeql file by simply adding the query to add characters to it. We can use for to add multiple characters like this:

  1. CREATE MIGRATION m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq
  2. ONTO m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a
  3. {
  4. for name in {'Alice', 'Billie', 'Cameron', 'Dana'}
  5. union (
  6. insert default::Character {
  7. name := name
  8. }
  9. );
  10. };

Trying to apply the data migration will produce the following reminder:

  1. $
  1. edgedb migrate
  1. edgedb error: could not read migrations in ./dbschema/migrations:
  2. could not read migration file ./dbschema/migrations/00002.edgeql:
  3. migration name should be
  4. `m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq` but
  5. `m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq` is used instead.
  6. Migration names are computed from the hash of the migration contents. To
  7. proceed you must fix the statement to read as:
  8. CREATE MIGRATION m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq
  9. ONTO ...
  10. if this migration is not applied to any database. Alternatively,
  11. revert the changes to the file.

The migration tool detected that we’ve altered the file and asks us to update the migration name (acting as a checksum) if this was deliberate. This is done as a precaution against accidental changes. Since we’ve done this on purpose, we can update the file and run edgedb migrate again.

  1. CREATE MIGRATION m1lclvwdpwitjj4xqm45wp74y4wjyadljct5o6bsctlnh5xbto74iq
  2. CREATE MIGRATION m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq
  3. ONTO m1xvu7o4z5f5xfwuun2vee2cryvvzh5lfilwgkulmqpifo5m3dnd6a
  4. {
  5. # ...
  6. };

After we apply the data migration we should be able to see the added characters:

  1. db>
  1. select Character {name};
  1. {
  2. default::Character {name: 'Alice'},
  3. default::Character {name: 'Billie'},
  4. default::Character {name: 'Cameron'},
  5. default::Character {name: 'Dana'},
  6. }

Let’s add a character class represented by a new type to our schema and data. Unlike in this scenario, we will add the required link class right away, without any intermediate properties. So we end up with a schema like this:

  1. type CharacterClass {
  2. required property name -> str;
  3. multi property skills -> str;
  4. }
  5. type Character {
  6. required property name -> str;
  7. required link class -> CharacterClass;
  8. }

We go ahead and try to apply this new schema:

  1. $
  1. edgedb migration create
  1. did you create object type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]
  2. > y
  3. did you create link 'class' of object type 'default::Character'?
  4. [y,n,l,c,b,s,q,?]
  5. > y
  6. Please specify an expression to populate existing objects in order to make
  7. link 'class' of object type 'default::Character' required:
  8. fill_expr>

Uh-oh! Unlike in a situation with a required property, it’s not a good idea to just insert a new CharacterClass object for every character. So we should abort this migration attempt and rethink our strategy. We need a separate step where the class link is not required so that we can write some custom queries to handle the character classes:

  1. type CharacterClass {
  2. required property name -> str;
  3. multi property skills -> str;
  4. }
  5. type Character {
  6. required property name -> str;
  7. link class -> CharacterClass;
  8. }

We can now create a migration for our new schema, but we won’t apply it right away:

  1. $
  1. edgedb migration create
  1. did you create object type 'default::CharacterClass'? [y,n,l,c,b,s,q,?]
  2. > y
  3. did you create link 'class' of object type 'default::Character'?
  4. [y,n,l,c,b,s,q,?]
  5. > y
  6. Created ./dbschema/migrations/00003.edgeql, id:
  7. m1jie3xamsm2b7ygqccwfh2degdi45oc7mwuyzjkanh2qwgiqvi2ya

We don’t need to create a blank migration to add data, we can add our modifications into the migration that adds the class link directly. Doing this makes sense when the schema changes seem to require the data migration and the two types of changes logically go together. We will need to create some CharacterClass objects as well as update the class link on existing Character objects:

  1. CREATE MIGRATION m1jie3xamsm2b7ygqccwfh2degdi45oc7mwuyzjkanh2qwgiqvi2ya
  2. ONTO m1juin65wriqmb4vwg23fiyajjxlzj2jyjv5qp36uxenit5y63g2iq
  3. {
  4. CREATE TYPE default::CharacterClass {
  5. CREATE REQUIRED PROPERTY name -> std::str;
  6. CREATE MULTI PROPERTY skills -> std::str;
  7. };
  8. ALTER TYPE default::Character {
  9. CREATE LINK class -> default::CharacterClass;
  10. };
  11. insert default::CharacterClass {
  12. name := 'Warrior',
  13. skills := {'punch', 'kick', 'run', 'jump'},
  14. };
  15. insert default::CharacterClass {
  16. name := 'Scholar',
  17. skills := {'read', 'write', 'analyze', 'refine'},
  18. };
  19. insert default::CharacterClass {
  20. name := 'Rogue',
  21. skills := {'impress', 'sing', 'steal', 'run', 'jump'},
  22. };
  23. # All warriors
  24. update default::Character
  25. filter .name in {'Alice'}
  26. set {
  27. class := assert_single((
  28. select default::CharacterClass
  29. filter .name = 'Warrior'
  30. )),
  31. };
  32. # All scholars
  33. update default::Character
  34. filter .name in {'Billie'}
  35. set {
  36. class := assert_single((
  37. select default::CharacterClass
  38. filter .name = 'Scholar'
  39. )),
  40. };
  41. # All rogues
  42. update default::Character
  43. filter .name in {'Cameron', 'Dana'}
  44. set {
  45. class := assert_single((
  46. select default::CharacterClass
  47. filter .name = 'Rogue'
  48. )),
  49. };
  50. };

In a real game we might have a lot more characters and so a good way to update them all is to update characters of the same class in bulk.

Just like before we’ll be reminded to fix the migration name since we’ve altered the migration file. After fixing the migration hash we can apply it. Now all our characters should have been assigned their classes:

  1. db>
  2. ...
  3. ...
  4. ...
  5. ...
  6. ...
  1. select Character {
  2. name,
  3. class: {
  4. name
  5. }
  6. };
  1. {
  2. default::Character {
  3. name: 'Alice',
  4. class: default::CharacterClass {name: 'Warrior'},
  5. },
  6. default::Character {
  7. name: 'Billie',
  8. class: default::CharacterClass {name: 'Scholar'},
  9. },
  10. default::Character {
  11. name: 'Cameron',
  12. class: default::CharacterClass {name: 'Rogue'},
  13. },
  14. default::Character {
  15. name: 'Dana',
  16. class: default::CharacterClass {name: 'Rogue'},
  17. },
  18. }

We’re finally ready to make the class link required. We update the schema:

  1. type CharacterClass {
  2. required property name -> str;
  3. multi property skills -> str;
  4. }
  5. type Character {
  6. required property name -> str;
  7. required link class -> CharacterClass;
  8. }

And we perform our final migration:

  1. $
  1. edgedb migration create
  1. did you make link 'class' of object type 'default::Character' required?
  2. [y,n,l,c,b,s,q,?]
  3. > y
  4. Please specify an expression to populate existing objects in order to
  5. make link 'class' of object type 'default::Character' required:
  6. fill_expr> assert_exists(.class)
  7. Created ./dbschema/migrations/00004.edgeql, id:
  8. m14yblybdo77c7bjtm6nugiy5cs6pl6rnuzo5b27gamy4zhuwjifia

The migration system doesn’t know that we’ve already assigned class values to all the Character objects, so it still asks us for an expression to be used in case any of the objects need it. We can use assert_exists(.class) here as a way of being explicit about the fact that we expect the values to already be present. Missing values would have caused an error even without the assert_exists wrapper, but being explicit may help us capture the intent and make debugging a little easier if anyone runs into a problem at this step.

In fact, before applying this migration, let’s actually add a new Character to see what happens:

  1. db>
  1. insert Character {name := 'Eric'};
  1. {
  2. default::Character {
  3. id: 9f4ac7a8-ac38-11ec-b076-afefd12d7e66,
  4. },
  5. }

Our attempt at migrating fails as we expected:

  1. $
  1. edgedb migrate
  1. edgedb error: MissingRequiredError: missing value for required link
  2. 'class' of object type 'default::Character'
  3. Detail: Failing object id is 'ee604992-c1b1-11ec-ad59-4f878963769f'.

After removing the bugged Character, we can migrate without any problems:

  1. $
  1. edgedb migrate
  1. Applied m14yblybdo77c7bjtm6nugiy5cs6pl6rnuzo5b27gamy4zhuwjifia
  2. (00004.edgeql)