Help Center » Developing Applications » The Complete Metaweb Application Development Reference Guide (API and MQL) » 5. The MQL Write Grammar

5. The MQL Write Grammar

Insertions, deletions and updates to a Metaweb database are expressed in a variant of the Metaweb Query Language documented in Chapter 3. The variant used for writing to Metaweb is known as the MQL write grammar, and is the subject of this chapter. The chapter begins with a long tutorial introduction to MQL writes, and then specifies the MQL write grammar more formally. Write queries are submitted to Metaweb via the mqlwrite service, which is covered in Chapter 6.

5.1. MQL Write Tutorial

MQL writes are represented as JSON objects, just as MQL reads are. A number of features of the MQL read grammar only make sense for reads and are not allowed in MQL writes. These include the use of [] to query an array of values, and the use of the sort, limit and optional directives. MQL write grammar supports two directives that are not allowed for reads. The create directive is used to create a new object in the database, and the connect directive is used to create a link between two objects. (As we'll see in the tutorial, however, the connect directive is sometimes implicit and need not be specified explicitly).

5.1.1. Creating a Type to Work With

Before we do any explicit MQL writes, let's begin by creating a simple type to work with. By creating and using your own type, you guarantee that the writes you try while working through this tutorial won't interact with writes being issued by other developers who may be working on the tutorial at the same time. As you know, Metaweb types are defined by regular Metaweb objects in the database. This means that types are created like any other objects, with MQL queries. Defining a type with raw MQL is difficult and error prone, however, so just about everyone defines types using the freebase.com client.

The type we're creating will represent musical notes, and we'll call it "note". In order to create it, follow the "My Freebase" link from the freebase.com home page. On the My Freebase page, click on "Types Created", and enter the name "Note". Figure 5.1 illustrates.

Figure 5.1. Creating a new type on freebase.com

Creating a new type on freebase.com

That's all you need to do for now. We'll add some properties to this type later, but now we just need the type itself. If you click on the name of the newly created type (note that the freebase.com client capitalizes the name for you) and look at the URL that it takes you to, you'll see that the name of the new type is /user/username/default_domain/note, where "username" is the username you logged in with. In the tutorial that follows, you'll see the username docs, but you should substitute your own name throughout.

5.1.2. Creating Objects

Let's begin with a very simple write query:

Write Result
{
  "create":"unless_exists",
  "type":
    "/user/docs/default_domain/note",
  "name":"A",
  "id":null
}
{
  "create":"created",
  "type":
    "/user/docs/default_domain/note",
  "name":"A",
  "id":"#9202a8c04000641f8000000000037ffc"
}

The first line of the query says that we want to create a new object, unless a matching object already exists. The second line specifies the type of the object we're creating (remember to substitute your own user name for "docs" here). The third line specifies a value for the name property of the new object. The fourth line of the write query is a request for the id of the newly created object. Asking for an id is the only way you are allowed to use null in a write query. You may not use null or [] for any other property.

Now let's look at the response to the write query. The first line is the create property, but its value has changed from unless_exists to created. This tells us that the object we specified did not already exist, and Metaweb has created it for us. The second and third lines simply repeat the type and name properties that we passed in. They don't provide any new information, but maintain the MQL invariant that responses have the same properties as queries. Finally, the fourth line returns the id of the newly created object.

Now let's see what happens if we run exactly the same query again:

Write Result
{
  "create":"unless_exists",
  "type":
    "/user/docs/default_domain/note",
  "name":"A",
  "id":null
}
{
  "create":"existed",
  "type":
    "/user/docs/default_domain/note",
  "name":"A",
  "id":"#9202a8c04000641f8000000000037ffc"
}

We're asking that an object be created unless it already exists. And this time it does already exist. So Metaweb returns the existed as the value of the create property, and returns the id of the already existing object. Note that this id is the same as the one we've already seen.

Now let's force Metaweb to create another new test object for us:

Write Result
{
  "create":"unconditional",
  "type":
    "/user/docs/default_domain/note",
  "name":"A",
  "id":null
}
{
  "create":"created",
  "type":
    "/user/docs/default_domain/note",
  "name":"A",
  "id":"#9202a8c04000641f800000000003800f"
}

In this query, we've changed the value of the create directive to unconditional. As its name implies, this value tells Metaweb to create a new object no matter what. Since a new object is created unconditionally, the value of the create property in the response will always be created. You can see that a new object was created by comparing the id returned by this query to those returned by the previous two queries.

We now have two note objects with the name "A". What happens if we run the original unless_exists write again?

{
  "code":"/api/status/error",
  "messages":[{
    "code":"/api/status/error/mql/result",
    "info":{
      "count":2,
"guids": [ "#9202a8c04000641f8000000000037ffc", "#9202a8c04000641f800000000003800f" ]     },
    "message":"Need a unique result to attach here, not 2",
    "path":"",
    "query":{
"create": "unless_exists", "type": "/user/docs/default_domain/note", "name": "A", "error_inside": ".", "id": null     }
  }]
}

The query fails this time, and returns the JSON object shown above. The "create":"unless_exists" directive works only if there are 0 or 1 instances of the object. If there is no object that matches, it creates one. If there is one object that matches, it returns it. But if there are more than one, it has no way to choose which one to return, and fails with an error message. Note that the query fails even if we omit "id":null. The lesson here is that if you plan to use unless_exists, you should use it consistently so you never end up with more than one instance of an object.

5.1.3. Connecting Objects

So far we've created two distinct objects with identical types and names. Let's now rename one so we can tell them apart by name. Recall that an object is named by linking it to a primitive value of /type/text. We want to update the name link to refer to a different value:

Write Result
{
  "id":"#9202a8c04000641f800000000003800f",
  "name":{
    "connect":"update",
    "value":"B",
    "lang":"/lang/en"
  }
}
{
  "id":"#9202a8c04000641f800000000003800f",
  "name":{
    "connect":"updated",
    "value":"B",
    "lang":"/lang/en"
  }
}

The first line of the query identifies, by id, the object we want to modify. The second and third lines specify that want to update the name property of that object so that it refers to the /type/text value specified by the 4th and 5th lines. (Recall that /type/text is a primitive value that consists of a string of text and a language identifier for that text. MQL write queries require you to specify both the value and lang properties when manipulating a name.)

The response looks just like the query except that the value of the connect property has changed to updated. This tells us that the update we requested has been performed.

What happens if we run exactly the same write query again?

Write Result
{
  "id":"#9202a8c04000641f800000000003800f",
  "name":{
    "connect":"update",
    "value":"B",
    "lang":"/lang/en"
  }
}
{
  "id":"#9202a8c04000641f800000000003800f",
  "name":{
    "connect":"present",
    "value":"B",
    "lang":"/lang/en"
  }
}

We're asking to make a change that has already been made, and Metaweb lets us know this by setting the connect property of the response to present.

We now have two newly-created objects with the same type and different names. We changed the name of the second object by updating a /type/text value. /type/text is a primitive type in Metaweb, so this isn't quite the same thing as a link between two different objects in the database. Now, let's modify the first object (the note A) so that it is a /common/topic in addition to being a note:

Write Result
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"insert",
    "id":"/common/topic"
  }
}
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"inserted",
    "id":"/common/topic"
  }
}

The first line of the query specifies the object to be modified. (Normally, we'd identify the object by name and type, but we can't specify the type and add a type in the same query. The name "A" is probably not unique by itself, so we specify the object we want to modify by id.) The second and third lines specify that we want to insert a new connection between this object and another object, and that this new connection should use the type property. The fourth line specifies, by id, the object that is being connected to.

Note that the value of the connect directive is insert instead of update, which is what we used above. The difference between the two is simple. Use "connect":"update" for properties that have a unique value (and for the name property, which is unique on a per-language basis). Use "connect":"insert" for properties, such as type, that can have more than one value. You are also allowed to use "connect":"insert" with unique properties if there is not already a value for that property.

The response object sets the value of the connect directive to inserted, telling us that the insertion was successful. Our note named "A" is now also a /common/topic. If you visit your "My Freebase" page, the note object should now be visible under the "Topics Created" heading. This is the main reason to use the /common/topic type on the objects you create: it allows them to work well with the freebase.com client.

What happens if we run the same query again?

Write Result
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"insert",
    "id":"/common/topic"
  }
}
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"present",
    "id":"/common/topic"
  }
}

We're asking to insert /common/topic into a set of types that already includes /common/topic, and we get the response present. It tells us that this value is already in the set and that nothing has changed. (Non-unique properties in Metaweb are like sets: they do not allow duplicates.)

Let's do a quick read query to confirm that our object is a member of two types:

Read Result
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":[]
}
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":[
    "/user/docs/default_domain/note",
    "/common/topic"
  ]
}

So we see that our object is, in fact, a note and a topic.

5.1.4. Disconnecting Objects

We've seen that Metaweb allows us to connect objects with "connect":"insert" or "connect":"update". To disconnect objects, use "connect":"delete". Let's alter the object that represents the note A again, to remove /common/topic from its set of types:

Write Result
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"delete",
    "id":"/common/topic"
  }
}
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"deleted",
    "id":"/common/topic"
  },
}

This query looks just like the query we used to add the type, except that we've changed "insert" to "delete". And Metaweb's response looks just like the response to the insertion, except that "inserted" has changed to "deleted". You can verify that the object is no longer a /common/topic by visiting "My Freebase" on sandbox.freebase.com and noting that it no longer appears in the "Topics Created" list.

At this point, you probably have a pretty good idea what will happen if we re-run the query:

Write Result
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"delete",
    "id":"/common/topic"
  }
}
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"absent",
    "id":"/common/topic"
  }
}

We asked Metaweb to remove /common/topic from a set that did not contain /common/topic, so it returned "absent" to indicate that nothing has been changed.

The MQL write grammar has no syntax for deleting objects themselves. The closest thing to deleting an object is to delete all connections from that object to others. If an object has no type, no name, and no other properties of interest, then it becomes effectively unreachable, and is almost as good as gone. Note, however, that Metaweb maintains a modification history for each object. When you view an object in the freebase.com client, you'll see a "History" link at the bottom of each page. Clicking this link allows you to view the change history for the object, and allows you to undo changes, including deletions.

When an object has had all its links deleted, it can still be queried by guid or creator (Metaweb does not allow these read-only properties to be deleted.) In practice, however, unreachable objects will only be found by determined searchers, and their continued existence is very unlikely to affect the results of future queries. Unreachable objects may at some point be purged from a Metaweb database, but their guids will never be reused.

Let's use this unlinking technique to "delete" the two note objects we've created:

Write Result
[{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"delete",
    "id":"/user/docs/default_domain/note"
  },
  "name":{
    "connect":"delete",
    "value":"A",
    "lang":"/lang/en"
  }
},{
  "id":"#9202a8c04000641f800000000003800f",
  "type":{
    "connect":"delete",
    "id":"/user/docs/default_domain/note"
  },
  "name":{
    "connect":"delete",
    "value":"B",
    "lang":"/lang/en"
  }
}]
[{
  "id":"#9202a8c04000641f8000000000037ffc",
  "type":{
    "connect":"deleted",
    "id":"/user/docs/default_domain/note"
  },
  "name":{
    "connect":"deleted",
    "value":"A",
    "lang":"/lang/en"
  }
},{
  "id":"#9202a8c04000641f800000000003800f",
  "type":{
    "connect":"deleted",
    "id":"/user/docs/default_domain/note"
  },
  "name":{
    "connect":"deleted",
    "value":"B",
    "lang":"/lang/en"
  }
}]

Note that this write query is really two separate queries, included within square brackets. The mqlwrite service (the topic of Chapter 6) accepts submissions of multiple writes at once. Note that names are deleted with "connect":"delete", even though they are unique and were originally created with "connect":"update". You must specify the lang property explicitly when deleting a name.

As a final test, let's query the first of these objects (by id) and find out what little information it still carries:

Read Result
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "*":null
}
{
  "id":"#9202a8c04000641f8000000000037ffc",
  "guid":"#9202a8c04000641f8000000000037ffc",
  "name":null,
  "type":[],
  "key":[],
  "creator":"/user/docs",
  "permission":"/boot/all_permission",
  "timestamp":"2006-11-08T20:00:02.0000Z"
 }

As expected, the name and types of the object are gone. All that remains are its id, creator, creation timestamp, and permissions.

Example 6.7 in Chapter 6 is a command-line script for unlinking Metaweb objects in this way. Its default behavior is to delete all objects you have created. You may find this script useful to wipe your slate clean while experimenting with MQL writes.

5.1.5. Writes and Default Properties

Take a look again at the MQL write queries we use to create and "delete" Note objects. First, the creation:

Write Result
{
  "create":"unless_exists",
  "type":"/user/docs/default_domain/note",
  "name":"C#",
  "id":null
}
{
  "create":"created",
  "type":"/user/docs/default_domain/note",
  "name":"C#",
  "id":"#9202a8c04000641f800000000104befe"
}

Now contrast this with the query that "deletes" the object by unlinking its type and name:

Write Result
{
  "id":"#9202a8c04000641f800000000104befe",
  "type":{
    "connect":"delete",
    "id":"/user/docs/default_domain/note"
  },
  "name":{
    "connect":"delete",
    "value":"C#",
    "lang":"/lang/en"
  }
}
{
  "id":"#9202a8c04000641f800000000104befe",
  "type":{
    "connect":"deleted",
    "id":"/user/docs/default_domain/note"
  },
  "name":{
    "connect":"deleted",
    "value":"C#",
    "lang":"/lang/en"
  }
}

The creation query is much more compact because we are able to specify the type as a single id and the name as a single string. In the deletion query, we must specify the expanded objects. There are three factors that interact to make the creation query shorter. First, recall from Chapter 3 that every type has a default property. For value types such as /type/text (the type of the name property) the default property is value. For core types in the /type domain, the default property is id. For all other types, the default property is name. So in the creation query, "type":"/user/docs/default_domain/note" is shorthand (but see the caution below!) for:

"type": { "id":"/user/docs/default_domain/note" }

The second factor that makes the creation query so compact is the fact that when you specify a default property rather than a full object in a MQL write query, Metaweb assumes an implicit "connect":"insert". So writing "type":"/user/docs/default_domain/note" is kind of (but not exactly: see the caution that follows) like writing:

"type": {
  "connect":"insert",
  "id":"/user/docs/default_domain/note"
}

The third factor that makes the creation query compact is that the language of /type/text values is automatically set to the default of English, or to your preferred language as specified by a parameter to the mqlwrite service. (See Chapter 6 for details.)

All three factors come into play when we write "name":"C#". "C#" becomes the value of the default property, which is the value. An implicit "connect":"insert" is added. And a lang property is added to specify /lang/en, or whatever language we are using. So "name":"C#" expands to (but see the caution!):

"name": {
  "connect":"insert",
  "value":"C#",
  "lang":"/lang/en"
}

5.1.6. Creating and Connecting More Objects

Let's try some more advanced examples. Before we start, though, we need to add a property to our Note type. Here's how you do it (Figure 5.2 illustrates):

  • Visit your "My Freebase" page on sandbox.freebase.com and click on the Note type under Types Created.

  • Click on the "Edit Type" link.

  • Click on the Add a New Property button, enter the property name "next" into the text field that appears, and click the Save button.

  • Freebase now allows you to enter details about the property:

    • Enter the type name "Note" into the Expected Type field (you may see a drop-down list containing your version of the Note type and many other developer's versions. Select the one that is followed by your username in parentheses.

    • Click the Restrict to one value checkbox to indicate that the property may have only a single value.

Figure 5.2. Adding a property on freebase.com

Adding a property on freebase.com

The next property we've just added to our note type allows us to link one note to another in a chain or a ring. We'll use this property to link each note to its perfect fifth--the note that is 7 semitones higher (usually, this is 5 white keys on a piano keyboard, which is probably why it is called a fifth.) If we start with the note C, we find that it's fifth is the note G. Before we start using the next property to represent fifths, however, let's run a simple query that will give us a convenient shortcut:

Write Result
{
  "id":"/user/docs/default_domain/note",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs",
    "value":"note"
  }
}
{
  "id":"/user/docs/default_domain/note",
  "key":{
    "namespace":"/user/docs",
    "connect":"inserted",
    "value":"note"
  }
}

This query specifies our Note type object by id, and then adds a new /type/key value to its key property. What we've done is to make /user/docs/note a synonym for /user/docs/default_domain/note. You may find this a helpful shortcut as you type in the example queries that follow. We'll explore namespaces again later in this tutorial.

Now, let's create Note objects to represent the notes C and G. Note that the following query is two independent queries in an array:

Write Result
[{
  "create":"unless_exists",
  "id":null,
  "type":"/user/docs/note",
  "name":"C"
},{
  "create":"unless_exists",
  "id":null,
  "type":"/user/docs/note",
  "name":"G"
}]
[{
  "create":"created",
  "id":"#9202a8c04000641f80000000000384b0",
  "type":"/user/docs/note",
  "name":"C"
},{
  "create":"created",
  "id":"#9202a8c04000641f80000000000384b4",
  "type":"/user/docs/note",
  "name":"G"
}]

We've asked Metaweb to create two Note objects, with names C and G, and to return their ids to us. Now, let's insert the link that indicates that G is the fifth of C:

Write Result
{
  "id":"#9202a8c04000641f80000000000384b0",
  "/user/docs/note/next":{
    "connect":"update",
    "id":"#9202a8c04000641f80000000000384b4"
  }
}
{
  "id":"#9202a8c04000641f80000000000384b0",
  "/user/docs/note/next":{
    "connect":"inserted",
    "id":"#9202a8c04000641f80000000000384b4"
  }
}

This compact query identifies both note objects by id and connects them with a connect directive. Since we defined the next property to be unique, it uses "connect":"update" instead of "connect":"insert". Note that since this query never specifies the type of the objects, we must use a fully-qualified property name for the next property. You can verify that this query did what we intended using the freebase.com client. Visit My Freebase on sandbox.freebase.com, and click on the Note type. On the page for the Note type, you should see a list of instances of that type. Click on the one named "C", and you'll see that it includes a hyperlink to the note G labeled "Next".

The linking technique shown above is straightforward and easy to understand. It uses one query to create (or look up) the two objects to be linked. Then it uses a second simple query to connect the two objects. It is usually possible, however, to combine the creation and linking into a single query. The following query, for example, sets the next property of the note G to a newly-created note named D:

Write Result
{
  "type":"/user/docs/note",
  "name":"G",
  "next":{
    "create":"unless_exists",
    "type":"/user/docs/note",
    "name":"D"
  }
}
{
  "type":"/user/docs/note",
  "name":"G",
  "next":{
    "create":"created",
    "type":"/user/docs/note",
    "name":"D"
  }
}

Notice that there is no connect directive here. Since the create directive is nested in this query, the connection is implicit.

Here's a longer query of the same sort:

Write Result
{
  "create":"unless_exists",
  "type":"/user/docs/note",
  "name":"B flat",
  "next":{
    "create":"unless_exists",
    "type":"/user/docs/note",
    "name":"F",
    "next":{
      "create":"unless_exists",
      "type":"/user/docs/note",
      "name":"C"
    }
  }
}
{
  "create":"created",
  "type":"/user/docs/note",
  "name":"B flat",
  "next":{
    "create":"created",
    "type":"/user/docs/note",
    "name":"F",
    "next":{
      "create":"connected",
      "type":"/user/docs/note",
      "name":"C"
    }
  }
}

This query creates a note F and links it to the existing note C, and then creates a note B flat and links it to the new note F. Note that the query uses "create":"unless_exists" three times. The response includes "created" twice for the newly created notes. But for the note C, which already exists, the response says "create":"connected". This tells us that the note C already existed, but that a new connection has been made to it. If we rerun the query, we get "create":"existed" all three times, since the objects and links already exist.

The following query is like the one above, but shorter, and with one important tweak:

Write Result
{
  "create":"unless_exists",
  "type":"/user/docs/note",
  "name":"E flat",
  "next":{
    "create":"unless_connected",
    "type":"/user/docs/note",
    "name":"B flat"
  }
}
{
  "create":"created",
  "type":"/user/docs/note",
  "name":"E flat",
  "next":{
    "create":"created",
    "type":"/user/docs/note",
    "name":"B flat"
  }
}

This query creates a new note E flat, and connects it to B flat. Notice, however, that in the nested clause of the query, we used a different form of the create directive: "create":"unless_connected". And in the response we have a "create":"created". If you examine the list of Note instances in the freebase.com client, you'll see that there are now two of them named "B flat". If you use unless_connected, then Metaweb looks for a matching object that is already connected. If it cannot find one, it creates a new one and connects it. In this case, there was an existing Note object named B flat, but it was not already connected, so the query created a new one. If we re-run the query, however, it simply returns "create":"existed" because the object and the connection exist.

Note that unless_connected only makes sense in nested clauses. If we change the outermost unless_exists in the query above to unless_connected, Metaweb complains: Can't use 'create': 'unless_connected' at the root of the query.

Let's clean up the extra B flat object we created:

Write Result
{
  "type":"/user/docs/note",
  "name":"E flat",
  "next":{
    "connect":"delete",
    "type":{
      "connect":"delete",
      "id":"/user/docs/note"
    },
    "name":{
      "connect":"delete",
      "value":"B flat",
      "lang":"/lang/en"
     }
  }
}
{
  "type":"/user/docs/note",
  "name":"E flat",
  "next":{
    "connect":"deleted",
    "type":{
      "connect":"deleted",
      "id":"/user/docs/note"
    },
    "name":{
      "connect":"deleted",
      "value":"B flat",
      "lang":"/lang/en"
    }
  }
}

Note that the query above does two things. It disconnects the name and type of the extra B flat object, and also disconnects that object from E flat. Now all we have to do is connect E flat to the valid B flat object. This should be easy for you now:

Write Result
{
  "type":"/user/docs/note",
  "name":"E flat",
  "next":{
    "connect":"insert",
    "type":"/user/docs/note",
    "name":"B flat"
  }
}
{
  "type":"/user/docs/note",
  "name":"E flat",
  "next":{
    "type":"/user/docs/note",
    "connect":"inserted",
    "name":"B flat"
  }
}

5.1.7. Review: Write Directives

At this stage of the tutorial, you've seen all the variations of the create and connect directives. Let's do a quick review before diving in to some more advanced examples.

The create directive comes in three forms:

"create":"unconditional"

Always create the specified object. It is almost never necessary or appropriate to use this form of the create directive.

"create":"unless_exists"

Look for the object in the database and create a new one if a match cannot be found.

"create":"unless_connected"

Look for a matching object that already exists and is already connected to the parent query. If no such object exists, create and connect a new one.

The possible responses to a create directive are the following:

"create":"created"

Indicates that a new object has been created. This is always the response for unconditional directives, but may also be returned by unless_exists and unless_connected directives.

"create":"existed"

Indicates that a pre-existing match was found and no object was created. This may be returned by unless_exists or unless_connected directives.

"create":"connected"

Indicates that the object already existed but a connection has been made. This response is only possible for unless_exists directives that are nested within a parent query.

The three forms of the connect directive are:

"connect":"insert"

Use this form to attach a value or object to a non-unique property. It can also be used to attach the first value or object to a unique property.

"connect":"update"

Use this form to attach a value or object to a unique property, replacing any value or object that was previously connected.

"connect":"delete"

Use this form to detach a value or object from a property. It works for unique and non-unique properties.

There are five possible responses to a connect query:

"connect":"inserted"

Indicates that an insert directive was successful.

"connect":"updated"

Indicates that an update directive was successful.

"connect":"deleted"

Indicates that a delete directive was successful.

"connect":"present"

Indicates that an insert or update directive was unsuccessful because the specified connection was already present.

"connect":"absent"

Indicates that a delete directive was not successful because the connection to be deleted did not exist.

5.1.8. Working with Sets

The most interesting examples we've explored so far have used the next property of our Note type. We defined this property to be unique--so that it can have only one value. There are some features of the MQL write grammar that only become apparent when used on non-unique properties, however. Let's define a Chord type and give it a non-unique property named note which links to Note objects. (By convention, we use a singular property name, even though we expect each Chord object to refer to multiple Note objects.) Create this type and its property on sandbox.freebase.com by repeating the steps we followed to define the Note type and its next property. Just change the names to Chord and note, and don't check the "Restrict to one value" box.

If you appreciated not having to type default_domain/ in the examples above, you can use the same shortcut for the new Chord type:

{
  "id":"/user/docs/default_domain/chord",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs",
    "value":"chord"
  }
}

Now let's define a chord using the notes C, E, and G.

Write Result
{
  "create":"unless_exists",
  "name":"CEG",
  "type":[
    "/common/topic",
    "/user/docs/chord"
  ],
  "note":[{
    "connect":"insert",
    "type":"/user/docs/note",
    "name":"C"
  },{
    "connect":"insert",
    "type":"/user/docs/note",
    "name":"G"
  },{
    "create":"unless_exists",
    "type":"/user/docs/note",
    "name":"E"
  }]
}
{
  "create":"created",
  "name":"CEG",
  "type":[
    "/common/topic",
    "/user/docs/chord"
  ],
  "note":[{
    "connect":"inserted",
    "type":"/user/docs/note",
    "name":"C"
  },{
    "connect":"inserted",
    "type":"/user/docs/note",
    "name":"G"
  },{
    "create":"created",
    "type":"/user/docs/note",
    "name":"E"
  }]
}

Several things immediately stand out about this query:

  • It specifies the ids of two types within a JSON array. The created object will be both a Chord and a Topic. (We'll say more about arrays in write queries and about the /common/topic type below).

  • It specifies three notes, as expanded objects, within a JSON array. These are the set of values for the note property of the chord.

  • Note objects C and G exist already, so this query uses "connect":"insert" for these two. We haven't created an object to represent E yet, so the query creates and connects it with "create":"unless_exists".

So far in this chapter, we've only seen square brackets in write queries when we were bundling up multiple top-level queries to be submitted to Metaweb in a single batch. The MQL write grammar is actually more general than this: nested queries can also be collected into an array, and this allows us to connect more than one value to a property. In the case of the type property, our query specifies two types by their id. As we discussed earlier, types can be specified by id because id is the default property of /type/type. When types are specified this way, "connect":"insert" is assumed. The reason that we specify /common/topic in addition to the Chord type is that the freebase.com client uses topics as its organizing metaphor. Objects of type /common/topic simply work better in the client. For example, /common/topic objects you create are listed under the heading "Topics Created" on your My Freebase page.

5.1.9. Bidirectional Links and Reciprocal Properties

One of the fundamental aspects of Metaweb is that all links between nodes are bi-directional. Our CEG Chord node has links to the nodes that represent the notes C, E, and G. Those links are bi-directional, which means that the C, E, and G nodes are linked to the CEG Chord node. The links are there, but our Note type doesn't define a appropriate property that exposes those links in the object-oriented view of the database.

Fortunately, the freebase.com client makes it very easy to define such a property:

  • Go to your My Freebase page on sandbox.freebase.com home page, and click on your Note type under User's Types.

  • Click on the "View Schema" link on the page for your Note type.

  • Look near the bottom of the schema page (you may have to scroll down) for the heading Suggested Properties. You should see something like what is shown in Figure 5.3

  • This tells you is that the type Chord has a property named Note. [13] The client is suggesting that you add a reciprocal property to expose the other direction of the link. The link is already there: all that is required is that you give this property a name so that you can refer to it.

  • Since the Chord property that refers to Notes is named note, it seems sensible to name the Note property that refers to Chords chord. Click the Edit button or double-click the double-click to edit text message. Then type in "chord" and hit Enter or click Save.

Figure 5.3. Adding a reciprocal property

Adding a reciprocal property

You have now created the property /user/docs/note/chord, which is the reciprocal property of /user/docs/chord/note. Since we now have a pair of properties, we can take advantage of the bi-directional nature of the links between chords and notes.

Let's experiment with this. First, we'll query the Chord CEG to find out what notes it contains:

Read Result
{
  "type":"/user/docs/chord",
  "name":"CEG",
  "note":[]
}
{
  "type":"/user/docs/chord",
  "name":"CEG",
  "note":["C","G","E"]
}

This result is unsurprising, given that the /user/docs/chord/note property is the one we defined originally. Now let's turn the query around and try out the reciprocal /user/docs/note/chord property we've just added. What chords is the note C a part of?

Read Result
{
  "type":"/user/docs/note",
  "name":"C",
  "chord":[]
}
{
  "type":"/user/docs/note",
  "name":"C",
  "chord":["CEG"]
}

The note C "knows" that it is part of the chord CEG even though we never set its chord property. Setting a property automatically causes its reciprocal property to be set as well. Because links are bi-directional in Metaweb, this is all automatic.

Now let's create a new chord:

Write Result
{
  "create":"unless_exists",
  "type":["/common/topic",
          "/user/docs/chord"],
  "name":"BFG"
}
{
  "create":"created",
  "type":["/common/topic",
          "/user/docs/chord"],
  "name":"BFG"
}

We've created a chord named BFG, but we haven't added the notes B, F and G to it. To further demonstrate reciprocal properties, we'll do the reverse, and add the chord to the notes:

Write Result
[{
  "create":"unless_exists",
  "type":"/user/docs/note",
  "name":"B",
  "chord": {
    "connect":"insert",
    "type":"/user/docs/chord",
    "name":"BFG"
  }
},{
  "create":"unless_exists",
  "type":"/user/docs/note",
  "name":"F",
  "chord": {
    "connect":"insert",
    "type":"/user/docs/chord",
    "name":"BFG"
  }
},{
  "create":"unless_exists",
  "type":"/user/docs/note",
  "name":"G",
  "chord": {
    "connect":"insert",
    "type":"/user/docs/chord",
    "name":"BFG"
  }
}]
[{
  "create":"created",
  "type":"/user/docs/note",
  "name":"B",
  "chord":{
    "connect":"inserted",
    "type":"/user/docs/chord",
    "name":"BFG"
  }
},{
  "create":"existed",
  "type":"/user/docs/note",
  "name":"F",
  "chord":{
    "connect":"inserted",
    "type":"/user/docs/chord",
    "name":"BFG"
  }
},{
  "create":"existed",
  "type":"/user/docs/note",
  "name":"G",
  "chord":{
    "connect":"inserted",
    "type":"/user/docs/chord",
    "name":"BFG"
  }
}]

This query connects the BFG chord to the chord property of the notes B, F, and G. (It also creates the note B, which didn't exist yet.) Now let's ask BFG what notes it contains:

Read Result
{
  "type":"/user/docs/chord",
  "name":"BFG",
  "note":[]
}
{
  "type":"/user/docs/chord",
  "name":"BFG",
  "note":["B","F","G"]
}

Once again, we've demonstrated that we can set a property of an object by setting the reciprocal property to refer to that object.

5.1.9.1. How Reciprocal Properties Work

Now let's explore the properties /user/docs/chord/note and /user/docs/note/chord to find out what makes them the reciprocal of each other. Recall that properties are themselves Metaweb objects. First we'll query the note property of Chord:

Read Result
{
  "type":"/type/property",
  "id":"/user/docs/chord/note",
  "expected_type":null,
  "master_property":null,
  "reverse_property":null
}
{
  "type":"/type/property",
  "id":"/user/docs/chord/note",
  "expected_type":"/user/docs/note",
  "master_property":null,
  "reverse_property":
    "/user/docs/default_domain/note/chord"
}

We find that the note property of Chord has an expected_type of Note, as expected. More interestingly, though, we find a property named reverse_property that refers to the chord property of the Note type. [14] So let's query that property now:

Read Result
{
  "type":"/type/property",
  "id":"/user/docs/note/chord",
  "expected_type":null,
  "master_property":null,
  "reverse_property":null
}
{
  "type":"/type/property",
  "id":"/user/docs/note/chord",
  "expected_type":"/user/docs/chord",
  "master_property":
    "/user/docs/default_domain/chord/note",
  "reverse_property":null
}

This property has a reverse_property of null, but has a property named master_property that refers back to the first property we looked at.

Reciprocal properties are linked to each other via the master_property and reverse_property properties. When one property is set, its reciprocal, if it has one, is automatically set. The reciprocity is symmetrical: the terms "master" and "reverse" imply a directionality or hierarchy, but the property labeled "master" has no special status or preference over the property labeled "reverse". (When types are created with the freebase.com clients, the property created first is the master property.) [15]

5.1.10. Writes and Ordered Collections

If a Metaweb property has not been declared a unique property, it may have a set of values. As we saw in Chapter 3, these sets may be ordered, and MQL read queries can access this order with the index keyword. This section shows how to define an ordering with a MQL write query. Not surprisingly, this is also uses the index keyword.

In order to demonstrate how to create an ordered collection, we'll need a suitable type. Chords don't work: the notes of a chord are played simultaneously, and no order is required. A broken chord (or arpeggio) is a chord in which the notes are played sequentially. Since there is a sequence, there is an order. Use the freebase.com client to define a new type named "Arpeggio" in the sandbox. Give it a property named "note" whose expected type is Note. Arpeggio is actually just like Chord: only the names are different. To save yourself typing, copy the type from /user/docs/default_domain/arpeggio (use your own username) to /user/docs/arpeggio, just as we did for the Note and Chord types:

{
  "id":"/user/docs/default_domain/arpeggio",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs",
    "value":"arpeggio"
  }
}

Now with our type defined, let's create our first ordered collection:

Write Result
{
  "create":"unless_exists",
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note": [{
    "index":0,
    "type":"/user/docs/note",
    "name":"C"
  },{
    "index":1,
    "type":"/user/docs/note",
    "name":"E"
  },{
    "index":2,
    "type":"/user/docs/note",
    "name":"G"
  }]
}
{
  "create":"created",
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note":[{
    "index":0,
    "type":"/user/docs/note",
    "name":"C"
  },{
    "index":1,
    "type":"/user/docs/note",
    "name":"E"
  },{
    "index":2,
    "type":"/user/docs/note",
    "name":"G"
  }]
}

Creating an arpeggio is much like creating a chord. Two things stand out about this query, however. First, each note has an index associated with it, and there are no connect directives. The index property specifies the ordering, and does an implicit "connect":"insert". (If the object was already inserted, then the index would simply re-order it without attempting to re-insert it.) If we were creating the Note objects at the same time as we were inserting them into this Arpeggio object, we would have to include both the create directive and the index property.

There are some strict rules that govern the use of the index property:

  • The index property may not appear within a top-level query. Indexes don't apply to objects but to the links between objects. The index property is used in sub-queries to specify the order of the links between the parent object and the children.

  • If there are n sibling sub-queries that specify an index, the values specified must include every integer from 0 to n-1. You must always start with zero. You may not include duplicate indexes, and you may not skip an index. It is not required that every element of a sub-query array have an index. Metaweb collections can be partially ordered and partially unordered.

This second rule may seem surprisingly strict, but remember that despite the name "index", the values we specify with the index property are not array indexes. The numbers are merely a simple way to specify a series of less than and greater than relationships. The requirement that indexes always run from 0 through n-1 means that there is really no way to insert an element at a given location with an ordered collection, and no way to move an element from one spot to another.

Suppose, for example, that we want to insert the notes B and F into our CEG arpeggio at the beginning so the arpeggio consists of the five sequential notes BFCEG:

Write Result
{
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note": [{
    "create":"unless_exists",
    "index":0,
    "type":"/user/docs/note",
    "name":"B"
  },{
    "create":"unless_exists",
    "index":1,
    "type":"/user/docs/note",
    "name":"F"
  }]
}
{
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note":[{
    "create":"connected",
    "index":0,
    "type":"/user/docs/note",
    "name":"B"
  },{
    "create":"connected",
    "index":1,
    "type":"/user/docs/note",
    "name":"F"
  }]
}

This query demonstrates that the index property can be used along with a create directive. The notes already exist, but are not connected so we get "create":"connected" in the response.

What has this query actually done? We can use a read query to ask for the notes of the arpeggio in order and see if we've accomplished what we wanted to. But before we do, let's consider what information we've given Metaweb. Previously, we told Metaweb that C comes before E and E comes before G. Now, we've also told Metaweb that B comes before F. But this isn't enough. Metaweb does not know anything about the relationship between BF and CEG. There are many possible orderings that meet the criteria we've specified. BFCEG is one possible ordering, but so is CEGBF, and so is CBEFG!

Let's see what we actually get:

Read Result
{
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note":[{
    "index":null,
    "name":null,
    "sort":"index"
  }]
}
{
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note":[
    {"index":0, "name":"B"},
    {"index":1, "name":"F"},
    {"index":2, "name":"C"},
    {"index":3, "name":"E"},
    {"index":4, "name":"G"}
  ]
}

Metaweb actually returns the notes in the order we wanted! The fact that Metaweb inserts new elements at the beginning of an ordered collection is an implementation detail, however, and is not behavior that is guaranteed. In practice, if you want to define particular ordering for the elements of a collection, you must write a single query that enumerates each of those elements and gives them all an index. Any subsequent insertions or shuffles require you to submit a new query that again lists all elements and defines their order. [16]

Here, therefore, is how we should have written the query above:

{
  "type":"/user/docs/arpeggio",
  "name":"broken CEG",
  "note": [{
    "create":"unless_exists",
    "index":0,
    "type":"/user/docs/note",
    "name":"B"
  },{
    "create":"unless_exists",
    "index":1,
    "type":"/user/docs/note",
    "name":"F"
  },
  { "index":2, "type":"/user/docs/note", "name":"C" },
  { "index":3, "type":"/user/docs/note", "name":"E" },
  { "index":4, "type":"/user/docs/note", "name":"G" }]
}

This query inserts the two new notes and re-iterates the order of the three existing ones, to specify a complete ordering of five notes with indexes 0 through 4.

Metaweb's ordered collections are not arrays and do not behave like arrays. The requirement that you re-specify the complete ordering even for simple insertions demonstrates this. And it also makes it clear that ordering is only practical for relatively small and static collections. It makes good sense to define an ordering for the tracks on an album, for example. It makes less sense to define an ordering for the albums recorded by a band, however, because this set of albums may change with time and a database maintainer would be required to respecify the complete discography each time a new album was added. And it makes no sense at all to try to specify an ordering for bands (by specifying an index for each instance property of the /music/artist type): there are simply too many of them.

5.1.11. Namespaces

In several places throughout this tutorial we've placed types into new namespaces simply to make our queries a little easier to enter into the query editor. In this section we'll explore namespaces in more detail.

We begin with a review of material from Chapter 2. First, remember that fully-qualified names and namespaces don't have anything to do with the name property of an object. The name property defines a human-readable display name for an object. Fully-qualified names are unique and can be used as an alternative to the object guid.

Fully-qualified names are defined by the value type /type/key. Every object has a key property that holds a set of /type/key values. If you want an object to have a fully-qualified name, insert a key into its key property. The value property of the key specifies the object's unqualified or local name. And the namespace property of the key specifies the object that defines the namespace. Any object can be a namespace: the only requirement is that the object must itself have a key. In this way we get a chain of /type/key/value properties that continues until we find a /type/key/namespace property that refers to the special root namespace object.

The type /type/namespace exists, and defines the the property /type/namespace/keys, which is the reciprocal of /type/key/namespace. Objects that are used as namespaces are usually given the type /type/namespace, but this is not required.

The reason that namespaces are useful is that namespaces allow us to use fully-qualified names to uniquely identify objects. If an object is given a key, then we can use its unique fully-qualified name as the value of the id property. Identifying objects with a human-readable id is simpler than using a long guid, and is more reliable than using the name and type properties together.

With that review of namespaces, let's try to put some of the note objects we've created into a namespace. We'll use the /user/docs/note type object as our namespace:

Write Result
[{
  "type":"/user/docs/note",
  "name":"C",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs/note",
    "value":"C"
  }
},{
  "type":"/user/docs/note",
  "name":"E",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs/note",
    "value":"E"
  }
},{
  "type":"/user/docs/note",
  "name":"G",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}]
[{
  "type":"/user/docs/note",
  "name":"C",
  "key":{
    "connect":"inserted",
    "namespace":"/user/docs/note",
    "value":"C"
  }
},{
  "type":"/user/docs/note",
  "name":"E",
  "key":{
    "connect":"inserted",
    "namespace":"/user/docs/note",
    "value":"E"
  }
},{
  "type":"/user/docs/note",
  "name":"G",
  "key":{
    "connect":"inserted",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}]

This query gives the notes C, E, and G keys named "C", "E", and "G" within the namespace /user/docs/note. That is, it defines fully-qualified names for these notes /user/docs/note/C, /user/docs/note/E, and /user/docs/note/G. Now that these notes have unique ids, it becomes (somewhat) easier to use them in queries. Here's how we might create a chord:

Write Result
{
  "create":"unless_exists",
  "type":"/user/docs/chord",
  "name":"CEG",
  "note":[{
    "connect":"insert",
    "id":"/user/docs/note/C"
  },{
    "connect":"insert",
    "id":"/user/docs/note/E"
  },{
    "connect":"insert",
    "id":"/user/docs/note/G"
  }]
}
{
  "create":"existed",
  "type":"/user/docs/chord",
  "name":"CEG",
  "note":[{
    "connect":"unchanged",
    "id":"/user/docs/note/C"
  },{
    "connect":"unchanged",
    "id":"/user/docs/note/E"
  },{
    "connect":"unchanged",
    "id":"/user/docs/note/G"
  }]
}

This query replaces the name and type properties of each note with a single id property. It doesn't actually do anything, since we have already created the CEG chord. We've seen that we can use a note's fully-qualified name as the value of its id property. What if we query the id of a note?

Read Result
{
  "type":"/user/docs/chord",
  "name":"CEG",
  "note":[{"id":null}]
}
{
  "type":"/user/docs/chord",
  "name":"CEG",
  "note":[{"id":"#9202a8c04000641f800000000104bd02"},
          {"id":"#9202a8c04000641f800000000104beae"},
          {"id":"#9202a8c04000641f800000000104beec"}]
}

We get the guids of the notes rather than the fully-qualified names we've just defined. Core types, such as /type/type and /type/property, that use id as their default property return a fully-qualified name instead of their guid in queries like this.

5.1.11.1. The /type/namespace/keys Property

We've seen that we can put objects into a namespace by setting the key property of the object. It is also possible to work with namespaces using the reciprocal property /type/namespace/keys. We've been using /user/docs/note as a namespace. This next query asks what keys it holds:

Read Result
{
  "id":"/user/docs/note",
  "/type/namespace/keys":[]
}
{
  "id":"/user/docs/note",
  "/type/namespace/keys":[
    "next","chord","arpeggio","C","E","G"
  ]
}

The namespace holds the names of the three notes we added, and also the names of the three properties we defined for the type. Let's repeat the query and ask for more detail:

Read Result
{
  "id":"/user/docs/note",
  "/type/namespace/keys":[{}]
}
{
  "id":"/user/docs/note",
  "/type/namespace/keys":[{
    "type":"/type/key",
    "namespace":
      "/user/docs/default_domain/note/next",
    "value":"next"
  },{
    "type":"/type/key",
    "namespace":
      "/user/docs/default_domain/note/chord",
    "value":"chord"
  },{
    "type":"/type/key",
    "namespace":
      "/user/docs/default_domain/note/arpeggio",
    "value":"arpeggio"
  },{
    "type":"/type/key",
    "namespace":
      "/user/docs/default_domain/note/C",
    "value":"C"
  },{
    "type":"/type/key",
    "namespace":
      "/user/docs/default_domain/note/E",
    "value":"E"
  },{
    "type":"/type/key",
    "namespace":
      "/user/docs/default_domain/note/G",
    "value":"G"
  }]
}

The values of the /type/namespace/keys property are /type/key values that have value and namespace properties. You'll notice that default_domain has crept back into the object ids in the query results. This is interesting, but not terribly important. We'll investigate it later in this section.

There is one very important point to notice about these query results. When a key value is used with /type/object/key, the namespace property is the id of the namespace object (such as /user/docs/note) that holds the key. But when a key value is used with /type/namespace/keys, the namespace property is the id of the object (such as /user/docs/note/C) contained by the namespace. This is important to understand, so we'll state it another way: suppose that an object o has a fully-qualified name in the namespace n. If we query the key property of o, we'll find a /type/key object whose namespace property refers to n. And if we query the /type/namespace/keys property of n, we'll find a /type/key object whose namespace property refers to o.

If you wanted to create a Metaweb namespace browser application, you could repeat the query above, starting with the id of the root namespace "/". The namespace properties of each of the returned keys specify the ids of all objects in the root namespace. If you recursively query each of these ids, you'll find the complete set of Metaweb objects with fully-qualified names.

It is also possible to add objects to namespaces using the /type/namespace/keys property instead of /type/object/key. The following query creates a new Note object named "G flat" and assigns it the fully-qualified name /user/docs/note/G_flat:

Write Result
{
  "id":"/user/docs/note",
  "/type/namespace/keys":{
    "connect":"insert",
    "value":"G_flat"
    "namespace":{
      "create":"unless_exists",
      "name":"G flat",
      "type":"/user/docs/note"
    }
  }
}
{
  "id":"/user/docs/note",
  "/type/namespace/keys":{
    "connect":"connected",
    "value":"G_flat",
    "namespace":{
      "create":"created",
      "name":"G flat",
      "type":"/user/docs/note"
    }
  }
}

5.1.11.2. Fully-Qualified Names and Uniqueness

In this section we explore the uniqueness of fully-qualified names. First, recall that earlier in the tutorial we defined shortcut names for types. We've been using the name /user/docs/note for a type that was originally defined as /user/docs/default_domain/note. If the type has two fully-qualified names, and we're using that type as a namespace, then each of the notes we inserted into that namespace should also have two names. /user/docs/default_domain/note/G should be the same thing as /user/docs/note/G:

Read Result
{
  "id":"/user/docs/default_domain/note/G",
  "/user/docs/note/chord":[]
}
{
  "id":"/user/docs/default_domain/note/G",
  "/user/docs/note/chord":["CEG","BFG"]
}

So a single object can have more than one fully-qualified name. But can a fully-qualified name refer to more than one object? Let's try to give the note F the same key that we assigned to G:

{
  "type":"/user/docs/note",
  "name":"F",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}

This query fails, although there is nothing obviously wrong with it. Metaweb simply will not allow the fully-qualified name /user/docs/note/G to refer to two different note objects. If you want to make /user/docs/note/G refer to the note F, you must first make sure that the note does not refer to the note G. This takes two queries. First, we must remove the fully-qualified name for the note G:

Write Result
{
  "id":"/user/docs/note/G",
  "key":{
    "connect":"delete",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}
{
  "id":"/user/docs/note/G",
  "key":{
    "connect":"deleted",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}

And then we can assign that fully-qualified name to the note F:

Write Result
{
  "type":"/user/docs/note",
  "name":"F",
  "key":{
    "connect":"insert",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}
{
  "type":"/user/docs/note",
  "name":"F",
  "key":{
    "connect":"inserted",
    "namespace":"/user/docs/note",
    "value":"G"
  }
}

Now if we were to ask for the name of the note /user/docs/note/G, we'd get "F". Making a fully-qualified name refer to another object is simpler if we use the /type/namespace/keys property instead. Here's how we could make /user/docs/note/G refer to the note G again:

Write Result
{
  "id":"/user/docs/note",
  "/type/namespace/keys": {
    "value":"G",
    "namespace":{
       "connect":"update",
       "type":"/user/docs/note",
       "name":"G"
    }
  }
}
{
  "id":"/user/docs/note",
  "/type/namespace/keys":{
    "value":"G",
    "namespace":{
      "connect":"updated",
      "type":"/user/docs/note",
      "name":"G"
    }
  }
}

This query locates the /type/key object that defines the name /user/docs/note/G, and updates the namespace property of that key, so that the name points to a different object. Note that you should not typically have to alter namespaces like this. Objects that have fully-qualified names should typically be constants.

Finally, notice that changing the object to which a fully-qualified name refers (as we did above) is a completely different operation than changing the fully-qualified name of an object. If we wanted to refer to the note G by the name /user/docs/note/Gnatural instead of /user/docs/note/G, we could do this:

Write Result
{
  "name":"G",
  "type":"/user/docs/note",
  "key":[{
    "connect":"delete",
    "namespace":"/user/docs/note",
    "value":"G"
  },{
    "connect":"insert",
    "namespace":"/user/docs/note",
    "value":"Gnatural"
  }]
}
{
  "name":"G",
  "type":"/user/docs/note",
  "key":[{
    "connect":"deleted",
    "namespace":"/user/docs/note",
    "value":"G"
  },{
    "connect":"inserted",
    "namespace":"/user/docs/note",
    "value":"Gnatural"
  }]
}

5.1.12. Properties, Types, and Domains

In this final section of the tutorial, we dig a deeper into Metaweb internals. Creating types and properties is almost always best done using the freebase.com client: there are a lot of details to get right, and correct setup of types and properties is critical for correct functioning. In general, it is better to be safe and let the client create types and properties for you.

On the other hand, types, properties, and domains are Metaweb objects just like any others, and they can be created and manipulated with MQL write queries. There is some educational value in seeing how this is done, and there are a few things that we can do with MQL that we cannot do through the client.

5.1.12.1. Creating Self-Referential Reciprocal Properties

The first property we defined in this tutorial was /user/docs/note/next, and we used it to model the cycle of fifths, so that the next property of the note C referred to G, and so on. We never created a reciprocal property for next, but it seems logical that the note G should have a previous property that refers to C.

At the time of this writing, the freebase.com client does not allow us to create reciprocal properties where both ends of the link refer the same type, but we can do it with MQL:

Write Result
{
  "id":"/user/docs/note",
  "/type/type/properties":{
    "create":"unless_exists",
    "type":"/type/property",
    "name":"Previous",
    "key":{
      "connect":"insert",
      "namespace":"/user/docs/note",
      "value":"previous"
    },
    "expected_type":
      "/user/docs/Note",
    "unique":true,
    "master_property":
      "/user/docs/note/next"
  }
}
{
  "id":"/user/docs/note",
  "/type/type/properties":{
    "create":"created",
    "type":"/type/property",
    "name":"Previous",
    "key":{
      "namespace":"/user/docs/note",
      "connect":"inserted",
      "value":"previous"
    },
    "expected_type":
      "/user/docs/Note",
    "unique":true,
    "master_property":
      "/user/docs/note/next"
  }
}

The first line of the query identifies the type to which we're adding the property. The second, third and fourth lines specify that we're creating a new /type/property object and connecting it to the properties property of our type. The 5th line gives the property the human-readable name "Previous" and the following four lines define a key so that the property has the fully-qualified name /user/docs/note/previous. The expected_type property specifies that the newly created property should link to other Note objects. The unique property specifies that each Note can have only a single value for the previous property. And, finally, the master_property property specifies that this new property is the reciprocal of /user/docs/note/next.

After executing this query, you can test it by querying the previous property of the note G. You can also use the freebase.com client to browse the note objects you've created and follow their next and previous properties back and forth.