Insertions, deletions and updates to a Metaweb database are expressed in a variant of the Metaweb Query Language that was 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. Write queries are submitted to Metaweb via the mqlwrite service, which is covered in Chapter 6.
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,
the use of directives like sort, limit return and optional, and
the use of operators like ~=.
The MQL write grammar does allow property prefixes like the read grammar does (though these are not often useful in writes) and defines two write-specific 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).
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 the freebase.com client provides an easier way to do it.
The type we're creating will represent musical notes, and we'll call it "note". Sign in to sandbox.freebase.com (creating an account if you have not already done so). From your homepage you can follow links to your default domain where you can create this new note type. Optionally, you may first want to create a new domain named "music" and put the note type into that domain. Don't add any properties to your new type yet. We'll do that later in this tutorial. This manual is focused on MQL and does not attempt to provide step-by-step descriptions of how to use the Freebase client to create domains, types, and properties. The Freebase UI is reasonably intuitive, however, and you can find detailed documentation at http://www.freebase.com/help.
The Freebase UI displays the names of types and their domains. To write MQL queries, however, we need to know type ids. Each Freebase user has a namespace of the form /user/name. Each user account initially has one domain, named default_domain under their namespace. If your username is "wanda", then the id of your default domain is /user/wanda/default_domain. If you used this default domain when creating the note type, the id of that type will be /user/wanda/default_domain/note. If you created a new domain named music, then the note type will have id /user/wanda/music/note. If you display details about your type in the Freebase client, you'll find the id of the type embedded in the URL displayed in your web browser's location bar. It might look like this:
http://sandbox.freebase.com/view/user/wanda/music/note
For the rest of this chapter, the queries will use types in the domain /user/docs/music. When you run the queries, replace "docs" with your own username, and, if you did not create your own music domain, replace "music" with "default_domain".
Let's begin with a very simple write query:
| Write | Result |
|---|---|
{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"A",
"id":null
}
|
{
"create":"created",
"type":"/user/docs/music/note",
"name":"A",
"id":"/guid/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 []) as the value of any property other than id (or guid).
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/music/note",
"name":"A",
"id":null
}
|
{
"create":"existed",
"type":"/user/docs/music/note",
"name":"A",
"id":"/guid/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/music/note",
"name":"A",
"id":null
}
|
{
"create":"created",
"type":"/user/docs/music/note",
"name":"A",
"id":"/guid/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",
"error_inside" : ".",
"id" : null,
"name" : "A",
"type" : "/user/docs/music/note"
}
}
]
}
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.
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":"/guid/9202a8c04000641f800000000003800f",
"name":{
"connect":"update",
"value":"B",
"lang":"/lang/en"
}
}
|
{
"id":"/guid/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":"/guid/9202a8c04000641f800000000003800f",
"name":{
"connect":"update",
"value":"B",
"lang":"/lang/en"
}
}
|
{
"id":"/guid/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":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"insert",
"id":"/common/topic"
}
}
|
{
"id":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"inserted",
"id":"/common/topic"
}
}
|
The first line of the query specifies the object to be modified. 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. In scripts that query links or use reflection (see Section 3.7) you may sometimes have to craft a MQL write query without knowing whether a particular property is unique or not. In that case, use "replace" instead of "update" or "insert". "connect":"replace" does an update for unique properties and does an insert for non-unique properties. "connect":"replace" is also useful if you have reason to believe that a property that is currently unique may in the future be relaxed to allow multiple values.
The response to the query above 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, which means that we can associate images, documents and aliases with the object.
What happens if we run the same query again?
| Write | Result |
|---|---|
{
"id":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"insert",
"id":"/common/topic"
}
}
|
{
"id":"/guid/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":"/guid/9202a8c04000641f8000000000037ffc",
"type":[]
}
|
{
"id":"/guid/9202a8c04000641f8000000000037ffc",
"type":[
"/user/docs/music/note",
"/common/topic"
]
}
|
So we see that our object is, in fact, a note and a topic.
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":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"delete",
"id":"/common/topic"
}
}
|
{
"id":"/guid/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".
At this point, you probably have a pretty good idea what will happen if we re-run the query:
| Write | Result |
|---|---|
{
"id":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"delete",
"id":"/common/topic"
}
}
|
{
"id":"/guid/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. [22]
When an object has had all its links deleted, it can still be queried by id, 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":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"delete",
"id":"/user/docs/music/note"
},
"name":{
"connect":"delete",
"value":"A",
"lang":"/lang/en"
}
},{
"id":"/guid/9202a8c04000641f800000000003800f",
"type":{
"connect":"delete",
"id":"/user/docs/music/note"
},
"name":{
"connect":"delete",
"value":"B",
"lang":"/lang/en"
}
}]
|
[{
"id":"/guid/9202a8c04000641f8000000000037ffc",
"type":{
"connect":"deleted",
"id":"/user/docs/music/note"
},
"name":{
"connect":"deleted",
"value":"A",
"lang":"/lang/en"
}
},{
"id":"/guid/9202a8c04000641f800000000003800f",
"type":{
"connect":"deleted",
"id":"/user/docs/music/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 and find out what little information it still carries:
| Read | Result |
|---|---|
{
"id":"/guid/9202a8c04000641f8000000000037ffc",
"*":null,
"/type/reflect/any_master": [{
"id":null,
"link":null,
"optional":true
}],
"/type/reflect/any_reverse": [{
"id":null,
"link":null,
"optional":true
}],
"/type/reflect/any_value": [{
"link":null,
"optional":true,
"value":null
}]
}
|
{
"id" : "/guid/9202a8c04000641f8000000000037ffc",
"guid" : "#9202a8c04000641f8000000000037ffc",
"type" : [],
"name" : null,
"key" : [],
"creator" : "/user/docs",
"permission" : "/boot/all_permission",
"timestamp" : "2008-08-29T23:42:08.0000Z",
"/type/reflect/any_master" : [{
"id" : "/boot/all_permission",
"link" : "/type/object/permission"
}],
"/type/reflect/any_reverse" : [],
"/type/reflect/any_value" : []
}
|
As expected, the name and types of the object are gone. All that remains are its id, creator, timestamp, and permission.
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/music/note",
"name":"C#",
"id":null
}
|
{
"create":"created",
"type":"/user/docs/music/note",
"name":"C#",
"id":"/guid/9202a8c04000641f800000000104befe"
}
|
Now contrast this with the query that "deletes" the object by unlinking its type and name:
| Write | Result |
|---|---|
{
"id":"/guid/9202a8c04000641f800000000104befe",
"type":{
"connect":"delete",
"id":"/user/docs/music/note"
},
"name":{
"connect":"delete",
"value":"C#",
"lang":"/lang/en"
}
}
|
{
"id":"/guid/9202a8c04000641f800000000104befe",
"type":{
"connect":"deleted",
"id":"/user/docs/music/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/music/note" is shorthand (but see the caution below!) for:
"type": { "id":"/user/docs/music/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/music/note" is kind of (but not exactly: see the caution that follows) like writing:
"type": {
"connect":"insert",
"id":"/user/docs/music/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.
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"
}
From the explanation above, you might assume that the compact creation query with which we began this section could be equivalently (but less compactly written) as:
{
"create":"unless_exists",
"id":null,
"name": "C#",
"type": {
"connect":"insert",
"id":"/user/docs/music/note"
}
}
If the queries used "create":"unconditional" then they would be the same. But the meaning of unless_exists is different for the two queries. The original compact query could be translated as If you can find a Note object named "C#", return its id. Otherwise, create a new Note object, name it "C#", and return its id.
But this variant that expands the type property is different in a subtle but important way. It tells Metaweb: find or create an object named "C#", and then add Note to its set of types. The difference between the two queries is critical if there is already an object (of type /programming/language, perhaps) with the name "C#".
Here's another way to think about this. When the type is specified by id, this is a constraint on the query. Metaweb must find an object that matches, or must construct one. When the type is specified in a sub-query with an explicit connect directive the sub-query is not a constraint, and does not affect the results of the unless_exists search.
In order to try some more advanced MQL write queries, we need to add a property to our Note type. Like types themselves, properties are just Metaweb objects, and can be created with MQL writes. It is tricky to do this in practice, however, and the freebase.com client makes it easy to add properties. Start by viewing the schema page for your Note type. You can navigate to this page using links in the client, or enter its URL directly into your browser:
http://sandbox.freebase.com/type/schema/user/docs/music/note
On this schema page, you'll find a user interface for adding new properties. Create a property with name and key set to next and with its expected type set to the Note type. Also, make the property unique, so that it is restricted to one value. This newly created property links one Note object to another, and we'll use it to link notes to their 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. 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/music/note",
"name":"C"
},{
"create":"unless_exists",
"id":null,
"type":"/user/docs/music/note",
"name":"G"
}]
|
[{
"create":"created",
"id":"/guid/9202a8c04000641f80000000000384b0",
"type":"/user/docs/music/note",
"name":"C"
},{
"create":"created",
"id":"/guid/9202a8c04000641f80000000000384b4",
"type":"/user/docs/music/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":"/guid/9202a8c04000641f80000000000384b0",
"/user/docs/music/note/next":{
"connect":"update",
"id":"/guid/9202a8c04000641f80000000000384b4"
}
}
|
{
"id":"/guid/9202a8c04000641f80000000000384b0",
"/user/docs/music/note/next":{
"connect":"inserted",
"id":"/guid/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 labeled "Next" to the note G.
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/music/note",
"name":"G",
"next":{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"D"
}
}
|
{
"type":"/user/docs/music/note",
"name":"G",
"next":{
"create":"created",
"type":"/user/docs/music/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/music/note",
"name":"B flat",
"next":{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"F",
"next":{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"C"
}
}
}
|
{
"create":"created",
"type":"/user/docs/music/note",
"name":"B flat",
"next":{
"create":"created",
"type":"/user/docs/music/note",
"name":"F",
"next":{
"create":"connected",
"type":"/user/docs/music/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/music/note",
"name":"E flat",
"next":{
"create":"unless_connected",
"type":"/user/docs/music/note",
"name":"B flat"
}
}
|
{
"create":"created",
"type":"/user/docs/music/note",
"name":"E flat",
"next":{
"create":"created",
"type":"/user/docs/music/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/music/note",
"name":"E flat",
"next":{
"connect":"delete",
"type":{
"connect":"delete",
"id":"/user/docs/music/note"
},
"name":{
"connect":"delete",
"value":"B flat",
"lang":"/lang/en"
}
}
}
|
{
"type":"/user/docs/music/note",
"name":"E flat",
"next":{
"connect":"deleted",
"type":{
"connect":"deleted",
"id":"/user/docs/music/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/music/note",
"name":"E flat",
"next":{
"connect":"insert",
"type":"/user/docs/music/note",
"name":"B flat"
}
}
|
{
"type":"/user/docs/music/note",
"name":"E flat",
"next":{
"connect":"inserted",
"type":"/user/docs/music/note",
"name":"B flat"
}
}
|
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":"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.
"create":"unconditional"
Always create the specified object. It is almost never necessary or appropriate to use this form of the create directive.
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 four 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":"replace"
This form does an update if the property is unique and does an insert otherwise. It is rarely necessary to use this type of connect.
"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.
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 you 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. The examples that follow assume that the id of the new Chord type is /user/docs/music/chord. You'll need to substitute your own username into the queries as you follow along.
Once the new type is created, let's define a chord using the notes C, E, and G:
| Write | Result |
|---|---|
{
"create":"unless_exists",
"name":"CEG",
"type":[
"/common/topic",
"/user/docs/music/chord"
],
"note":[{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"C"
},{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"G"
},{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"E"
}]
}
|
{
"create":"created",
"name":"CEG",
"type":[
"/common/topic",
"/user/docs/music/chord"
],
"note":[{
"create":"connected",
"type":"/user/docs/music/note",
"name":"C"
},{
"create":"connected",
"type":"/user/docs/music/note",
"name":"G"
},{
"create":"created",
"type":"/user/docs/music/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 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 the response to the "create":"unless_exists" directive for these two notes is "create":"connected" to indicate that an already-existing object was connected. (Since we knew ahead of time that these two notes already existed, we could have used "connect":"insert" instead.) Since note E did not exist, the response includes "connect":"created" indicating that the note object was created and connected.
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.
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.
The Freebase client makes it very easy to define such a property. View the schema of your note type by entering a URL like this:
http://sandbox.freebase.com/type/schema/user/docs/music/note
In addition to listing the properties defined by the Note type, this page also lists the "incoming properties" that have Note as their expected type. This list of incoming properties includes an option to create a reciprocal or "return property". A good name for the reciprocal of the chord/note property is, of course, note/chord. Since you now have a pair of properties, you 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/music/chord",
"name":"CEG",
"note":[]
}
|
{
"type":"/user/docs/music/chord",
"name":"CEG",
"note":["C","G","E"]
}
|
This result is unsurprising, given that the /user/docs/music/chord/note property is the one we defined originally. Now let's turn the query around and try out the reciprocal /user/docs/music/note/chord property we've just added. What chords is the note C a part of?
| Read | Result |
|---|---|
{
"type":"/user/docs/music/note",
"name":"C",
"chord":[]
}
|
{
"type":"/user/docs/music/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/music/chord"],
"name":"BFG"
}
|
{
"create":"created",
"type":["/common/topic",
"/user/docs/music/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/music/note",
"name":"B",
"chord": {
"connect":"insert",
"type":"/user/docs/music/chord",
"name":"BFG"
}
},{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"F",
"chord": {
"connect":"insert",
"type":"/user/docs/music/chord",
"name":"BFG"
}
},{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"G",
"chord": {
"connect":"insert",
"type":"/user/docs/music/chord",
"name":"BFG"
}
}]
|
[{
"create":"created",
"type":"/user/docs/music/note",
"name":"B",
"chord":{
"connect":"inserted",
"type":"/user/docs/music/chord",
"name":"BFG"
}
},{
"create":"existed",
"type":"/user/docs/music/note",
"name":"F",
"chord":{
"connect":"inserted",
"type":"/user/docs/music/chord",
"name":"BFG"
}
},{
"create":"existed",
"type":"/user/docs/music/note",
"name":"G",
"chord":{
"connect":"inserted",
"type":"/user/docs/music/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/music/chord",
"name":"BFG",
"note":[]
}
|
{
"type":"/user/docs/music/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.
We began this section by creating the /user/docs/music/note/chord property as the explicit reciprocal of /user/docs/music/chord/note. This step is not actually necessary, however. Metaweb can traverse a link in the reverse direction even if a property describing that direction does not exist. MQL also allows us to refer to the reciprocal of a property by prefixing the property id with an exclamation mark. So in the queries above, we could replace the property chord with !/user/docs/music/chord/note. See Section 3.4.4 for further discussion.
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 directive. This section shows how to define an ordering with a MQL write query. Not surprisingly, this is also uses the index directive.
To demonstrate, we'll use our Chord type to represent arpeggios. An arpeggio (or "broken chord") is a set of notes played sequentially rather than simultaneously. Since there is a sequence, there is an order, and we'll use the index directive to specify the order in which the notes should be played. Here's how we might create a chord with ordered notes:
| Write | Result |
|---|---|
{
"create":"unless_exists",
"type":"/user/docs/music/chord",
"name":"broken CEG",
"note": [{
"index":0,
"type":"/user/docs/music/note",
"name":"C"
},{
"index":1,
"type":"/user/docs/music/note",
"name":"E"
},{
"index":2,
"type":"/user/docs/music/note",
"name":"G"
}]
}
|
{
"create":"created",
"type":"/user/docs/music/chord",
"name":"broken CEG",
"note":[{
"index":0,
"type":"/user/docs/music/note",
"name":"C"
},{
"index":1,
"type":"/user/docs/music/note",
"name":"E"
},{
"index":2,
"type":"/user/docs/music/note",
"name":"G"
}]
}
|
Two things stand out about this query: each note has an index associated with it, and there are no connect directives. The index directive specifies the ordering. Remember that this ordering is not a property of the Chord, nor of the Note objects that comprise it. Instead, the indexes are properties of the links between the chord and the notes. It is not surprising, then, that using the index directive in a write query implicitly specifies "connect":"insert" for that query. You can use the index directive even for object that have already been inserted: in this case, the index directive simply re-orders the object without attempting to re-insert it. If we were creating the Note objects at the same time as we were inserting them into this Chord, we would have to include both the create directive and the index directive.
There are some strict rules that govern the use of the index directive in write queries:
The index directive may not appear within a top-level query. Indexes don't apply to objects but to the links between objects. The index directive 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 directive 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 no way to insert an element at a given location with an ordered collection. All we can do is move elements to (or insert new elements at) the beginning of the list. Here's how we would change our CEG arpeggio into a GCE arpeggio, for example:
| Write | Result |
|---|---|
{
"type":"/user/docs/music/chord",
"name":"broken CEG",
"note": [{
"index":0,
"type":"/user/docs/music/note",
"name":"G"
}]
}
|
{
"type" : "/user/docs/music/chord",
"name" : "broken CEG",
"note" : [{
"index" : 0,
"type" : "/user/docs/music/note"
"name" : "G",
}]
}
|
In this query, the response is identical to the query: there is no "index":"reordered" property in the response to let us know that our query succeeded. But we can check with a simple read:
| Read | Result |
|---|---|
{
"type":"/user/docs/music/chord",
"name":"broken CEG",
"note":[{
"index":null,
"name":null,
"sort":"index"
}]
}
|
{
"type" : "/user/docs/music/chord",
"name" : "broken CEG",
"note" : [
{"index" : 0, "name" : "G"},
{"index" : 1, "name" : "C"},
{"index" : 2, "name" : "E"}
]
}
|
Now, let's add two more notes to beginning of the arpeggio. 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:
| Write | Result |
|---|---|
{
"type":"/user/docs/music/chord",
"name":"broken CEG",
"note": [{
"create":"unless_exists",
"index":0,
"type":"/user/docs/music/note",
"name":"B"
},{
"create":"unless_exists",
"index":1,
"type":"/user/docs/music/note",
"name":"F"
}]
}
|
{
"type":"/user/docs/music/chord",
"name":"broken CEG",
"note":[{
"create":"connected",
"index":0,
"type":"/user/docs/music/note",
"name":"B"
},{
"create":"connected",
"index":1,
"type":"/user/docs/music/note",
"name":"F"
}]
}
|
If you repeat the read query from above, you'll see that the sequence of notes in our arpeggio is now BFGCE.
Metaweb's ordered collections are not random-access arrays and do not behave that way. In read queries, you cannot ask for the object with a specific index, you can only sort by index (and optionally limit the number of results.) And in writes, you cannot insert an element at a specified index unless you specify the index of all elements that come before it.
By placing an object in a namespace we define a fully-qualified name for it, and that name can be used as the value of the id property to uniquely identify the object. In this section we'll demonstrate how to do this and explore namespaces and enumerations 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 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 defines the property /type/namespace/keys, which is the reciprocal of /type/key/namespace. Namespaces also have a unique property. If true, the namespace may not contain two names for the same object. 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 meaningful id is simpler than using a long string of hexadecimal digits in the /guid pseudo-namespace.
Now that we've reviewed namespaces, let's create a namespace in which we can define names for our Note objects. Since notes are of type /user/docs/music/note, let's use the plural form /user/docs/music/notes as the id of the namespace. Here's how we create the new namespace object and insert it into /user/docs/music (using our domain object as a namespace):
| Write | Result |
|---|---|
{
"id":"/user/docs/music",
"/type/namespace/keys": {
"value":"notes",
"namespace": {
"create":"unless_connected",
"type":"/type/namespace",
"unique":false
}
}
}
|
{
"id" : "/user/docs/music",
"/type/namespace/keys" : {
"value" : "notes",
"namespace" : {
"create" : "created",
"type" : "/type/namespace",
"unique" : false
}
}
}
|
Now let's put some of the note objects we've created into our new namespace:
| Write | Result |
|---|---|
[{
"type":"/user/docs/music/note",
"name":"C",
"key":{
"connect":"insert",
"namespace":"/user/docs/music/notes",
"value":"C"
}
},{
"type":"/user/docs/music/note",
"name":"E",
"key":{
"connect":"insert",
"namespace":"/user/docs/music/notes",
"value":"E"
}
},{
"type":"/user/docs/music/note",
"name":"G",
"key":{
"connect":"insert",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}]
|
[{
"type":"/user/docs/music/note",
"name":"C",
"key":{
"connect":"inserted",
"namespace":"/user/docs/music/notes",
"value":"C"
}
},{
"type":"/user/docs/music/note",
"name":"E",
"key":{
"connect":"inserted",
"namespace":"/user/docs/music/notes",
"value":"E"
}
},{
"type":"/user/docs/music/note",
"name":"G",
"key":{
"connect":"inserted",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}]
|
This query gives the notes C, E, and G keys named "C", "E", and "G" within the namespace /user/docs/music/notes. That is, it defines fully-qualified names for these notes /user/docs/music/notes/C, /user/docs/music/notes/E, and /user/docs/music/notes/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/music/chord",
"name":"CEG",
"note":[{
"connect":"insert",
"id":"/user/docs/music/notes/C"
},{
"connect":"insert",
"id":"/user/docs/music/notes/E"
},{
"connect":"insert",
"id":"/user/docs/music/notes/G"
}]
}
|
{
"create":"existed",
"type":"/user/docs/music/chord",
"name":"CEG",
"note":[{
"connect":"present",
"id":"/user/docs/music/notes/C"
},{
"connect":"present",
"id":"/user/docs/music/notes/E"
},{
"connect":"present",
"id":"/user/docs/music/notes/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/music/chord",
"name":"CEG",
"note":[{"id":null}]
}
|
{
"type" : "/user/docs/music/chord",
"name" : "CEG",
"note" : [
{"id" : "/user/docs/music/notes/C"},
{"id" : "/user/docs/music/notes/G"},
{"id" : "/user/docs/music/notes/E"}
]
}
|
Our query now returns the ids we just defined rather returning an id from the /guid namespace as it would have done before these ids were defined.
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/music/note as a namespace. This next query asks what keys it holds:
| Read | Result |
|---|---|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys":[]
}
|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys":[
"C","E","G"
]
}
|
The namespace holds the local names of the three notes we added. Let's repeat the query and ask for more detail:
| Read | Result |
|---|---|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys":[{}]
}
|
{
"id" : "/user/docs/music/note",
"/type/namespace/keys" : [{
"type" : "/type/key",
"namespace" : "/user/docs/music/notes/C",
"value" : "C"
},{
"type" : "/type/key",
"namespace" : "/user/docs/music/notes/E",
"value" : "E"
},{
"type" : "/type/key",
"namespace" : "/user/docs/music/notes/G",
"value" : "G"
}]
}
|
The values of the /type/namespace/keys property are /type/key values that have value and namespace properties.
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/music/notes) 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/music/notes/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/music/notes/G_flat:
| Write | Result |
|---|---|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys":{
"connect":"insert",
"value":"G_flat",
"namespace":{
"create":"unless_exists",
"name":"G flat",
"type":"/user/docs/music/note"
}
}
}
|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys":{
"connect":"inserted",
"value":"G_flat",
"namespace":{
"create":"created",
"name":"G flat",
"type":"/user/docs/music/note"
}
}
}
|
The key feature of fully-qualified names is their uniqueness: two objects simply cannot share the same id. In this section we experiment with uniqueness and demonstrate how to change the object to which an id refers and how to change the id of an object.
First, let's try to give the note F the same key that we assigned to G:
{
"type":"/user/docs/music/note",
"name":"F",
"key":{
"connect":"insert",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}
This query is syntactically valid JSON, and semantically valid MQL, but it fails with the error message "This value is already in use. Please delete it first": Metaweb simply will not allow the fully-qualified name /user/docs/music/notes/G to refer to two different note objects. If you want to make /user/docs/music/notes/G refer to the note F, you must first make sure that that 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/music/notes/G",
"key":{
"connect":"delete",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}
|
{
"id":"/user/docs/music/notes/G",
"key":{
"connect":"deleted",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}
|
And then we can assign that fully-qualified name to the note F:
| Write | Result |
|---|---|
{
"type":"/user/docs/music/note",
"name":"F",
"key":{
"connect":"insert",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}
|
{
"type":"/user/docs/music/note",
"name":"F",
"key":{
"connect":"inserted",
"namespace":"/user/docs/music/notes",
"value":"G"
}
}
|
Now if we were to ask for the name of the note /user/docs/music/notes/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/music/note/G refer to the note G again. Note that only one query is required if we do it this way:
| Write | Result |
|---|---|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys": {
"value":"G",
"namespace":{
"connect":"update",
"type":"/user/docs/music/note",
"name":"G"
}
}
}
|
{
"id":"/user/docs/music/notes",
"/type/namespace/keys":{
"value":"G",
"namespace":{
"connect":"updated",
"type":"/user/docs/music/note",
"name":"G"
}
}
}
|
This query locates the /type/key object that defines the name /user/docs/music/notes/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/music/note/Gnatural instead of /user/docs/music/note/G, we could do this:
| Write | Result |
|---|---|
{
"name":"G",
"type":"/user/docs/music/note",
"key":[{
"connect":"delete",
"namespace":"/user/docs/music/notes",
"value":"G"
},{
"connect":"insert",
"namespace":"/user/docs/music/notes",
"value":"Gnatural"
}]
}
|
{
"name":"G",
"type":"/user/docs/music/note",
"key":[{
"connect":"deleted",
"namespace":"/user/docs/music/notes",
"value":"G"
},{
"connect":"inserted",
"namespace":"/user/docs/music/notes",
"value":"Gnatural"
}]
}
|
Since we did not set the unique property of our /user/docs/music/notes namespace to true, it is perfectly legal for one note to have two names (such as "G" and "Gnatural") in the namespace.
Suppose we want all of our Note objects to have a fully-qualified name in the /user/docs/music/notes namespace. We can simplify the process of defining these fully-qualified names by giving the Note type a property of /type/enumeration. (See Section 3.3.5 for a review of enumerations).
Begin by adding a new property to the Note type using the freebase.com client on the sandbox. Name the property "lname" (for "local name") and set its expected type to /type/enumeration. Since our /user/docs/music/notes namespace is not unique (it allows multiple keys to refer to the same object) don't make this new lname property be unique.
At the time of this writing, the Freebase client does not allow you to specify the namespace associated with this lname property, and we have to make our own write query to specify that. When you try this yourself, you may find that you are able to enter the /user/docs/music/notes namespace directly into the client. If so that is all you need to do. If not, execute this query with the query editor on the sandbox:
| Write | Result |
|---|---|
{
"id":"/user/docs/music/note/lname",
"/type/property/enumeration": {
"connect":"update",
"id:"/user/docs/music/notes"
}
}
|
{
"id" : "/user/docs/music/note/lname",
"/type/property/enumeration" : {
"connect" : "inserted",
"id" : "/user/docs/music/notes"
}
}
|
With this new property defined and linked to our namespace, we can query the names, ids, and lnames of our notes. Note objects that have already been given fully-qualified names in our namespace automatically have their lname property defined. The following read query (and partial set of results) demonstrate:
| Read | Result |
|---|---|
[{
"type" : "/user/docs/music/note",
"name" : null,
"id" : null,
"lname" : null
}]
|
[{
"type" : "/user/docs/music/note",
"name" : "C",
"id" : "/user/docs/music/notes/C",
"lname" : "C"
},{
"type" : "/user/docs/music/note",
"name" : "G",
"id" : "/user/docs/music/notes/Gnatural",
"lname" : "Gnatural"
},{
"type" : "/user/docs/music/note",
"name" : "F",
"id" : "/guid/9202a8c04000641f800000000901ca07",
"lname" : null
}]
|
With the lname enumeration defined, it becomes simple to create a new Note object and give it a fully-qualified name at the same time:
| Write | Result |
|---|---|
{
"create":"unless_exists",
"type":"/user/docs/music/note",
"name":"F sharp",
"lname":"Fsharp",
"id":null
}
|
{
"create" : "created",
"type" : "/user/docs/music/note",
"name" : "F sharp",
"lname" : "Fsharp",
"id" : "/user/docs/music/notes/Fsharp"
}
|
Specifying a value for lname is all we need to do to define a fully-qualified name for the new object. Note that the id query we included in the write returns the fully-qualified name we defined rather than the guid of the object.
To define a lname for an already existing object, you can use a query like this one, which creates a second fully-qualified name for the Note object we just defined:
| Write | Result |
|---|---|
{
"id":"/user/docs/music/notes/Fsharp",
"type":"/user/docs/music/note",
"lname":{
"connect":"insert",
"value":"sharpf"
}
}
|
{
"id" : "/user/docs/music/notes/Fsharp",
"type" : "/user/docs/music/note",
"lname" : {
"connect" : "inserted",
"value" : "sharpf"
},
}
|
You can check that this worked with a simple read query:
| Read | Result |
|---|---|
{
"id" : "/user/docs/music/notes/sharpf",
"/user/docs/music/note/lname" : [],
}
|
{
"id" : "/user/docs/music/notes/sharpf",
"/user/docs/music/note/lname" : [
"Fsharp",
"sharpf"
]
}
|
As you know, Metaweb databases are completely open for reading: no authentication is required, and no access control is performed for reads. That is not the case for writes, but so far in this chapter we've ignored access control issues. You've been creating instances of types you've defined yourself, and assuming that you've logged into the sandbox correctly, you've always been able to perform write queries. In practice, however, you may often need to work with domains, types and objects created by someone else, and you need to understand Metaweb's access control mechanism in order to know which writes are allowed and which are forbidden.
The /type/user type represents a single Metaweb user. The /type/usergroup type represents a set or group of users. And the /type/permission type represents a set of usergroups. A /type/permission object can be called a "permission group". Every object has a permission property that specifies its permission group. Only users in that permission group are allowed to modify the object. (This over-simplifies Metaweb's access control model a bit: details on per-object access control and per-property access control appear below.)
Your /type/user object has an associated usergroup and permission group that grants you permission to modify the object, and prevents most other users from modifying your object. You can look up the name and ids of your own usergroup and permission group with a read query like this one:
| Read | Result |
|---|---|
{
"id":"/user/docs",
"permission": {
"id":null,
"name":null,
"permits":[{
"id":null,
"name":null,
"member":[]
}]
}
}
|
{
"id" : "/user/docs",
"permission" : {
"id" : "/guid/9202a8c04000641f800000000120900f",
"name" : null,
"permits" : [{
"id" : "/boot/user_administrator_group",
"name" : null,
"member" : ["/user/user_administrator"]
},{
"id" : "/guid/9202a8c04000641f800000000120901b",
"name" : "docs's private user group",
"member" : ["/user/docs"]
}]
}
}
|
Furthermore, when you create a new domain using the freebase.com client, a new permission group and two new user groups are created to control access to the domain. We can explore this by re-running the query above for the /user/docs/music domain object. The results shown here omit some of the administrative members of the /boot/schema_group usergroup:
| Read | Result |
|---|---|
{
"id":"/user/docs/music",
"permission": {
"id":null,
"name":null,
"permits":[{
"id":null,
"name":null,
"member":[]
}]
}
}
|
{
"id" : "/user/docs/music",
"permission" : {
"id" : "/guid/9202a8c04000641f800000000903db10",
"name" : null,
"permits" : [{
"id" : "/guid/9202a8c04000641f800000000903db14",
"name" : "Owners of music domain",
"member" : ["/user/docs"]
},{
"id" : "/guid/9202a8c04000641f800000000903db1a",
"name" : "Music Experts",
"member" : []
},{
"id" : "/boot/schema_group",
"name" : null,
"member" : [
"/user/domain_administrator",
"/user/typelibrarian",
"/user/delete_bot",
"/user/merge_bot"
]
}]
}
}
|
You might also be interested to know what usergroups you are part of and what permissions groups those usergroups are part of:
| Read | Result |
|---|---|
{
"id":"/user/docs",
"/type/user/usergroup":[{
"id":null,
"name":null,
"/type/usergroup/permitted":[{
"id":null,
"name":null
}]
}]
}
|
{
"id" : "/user/docs",
"/type/user/usergroup" : [{
"id" : "/boot/all_group",
"name" : "Global User Group",
"/type/usergroup/permitted" : [{
"id" : "/boot/all_permission",
"name" : "Global Write Permission"
}]
},{
"id" : "/guid/9202a8c04000641f800000000120901b",
"name" : "docs's private user group",
"/type/usergroup/permitted" : [{
"id" : "/guid/9202a8c04000641f800000000120900f",
"name" : null
},{
"id" : "/guid/9202a8c04000641f8000000001209021",
"name" : null
}]
},{
"id" : "/guid/9202a8c04000641f8000000001209025",
"name" : "Owners of docs's default types",
"/type/usergroup/permitted" : [{
"id" : "/guid/9202a8c04000641f8000000001209021",
"name" : null
}]
},{
"id" : "/guid/9202a8c04000641f800000000903db14",
"name" : "Owners of music domain",
"/type/usergroup/permitted" : [{
"id" : "/guid/9202a8c04000641f800000000903db10",
"name" : null
}]
}]
}
|
All Metaweb users (in good standing) are part of the /boot/all_group usergroup and therefore part of the /boot/all_permission permission group. By default, new objects created with MQL write queries are given a permission of /boot/all_permission. We'll learn how to alter this default and specify a more restrictive permission in Chapter 6. MQL write queries are not allowed to alter the permission property of any object, so once an object is created, its permission is fixed. It is possible, of course, to alter the membership of a permission group or a usergroup, and the freebase.com client defines a UI for adding users to domain-related usergroups.
Every Metaweb object has a permission property that specifies the /type/permission object that controls access to it. The fundamental Metaweb access control rule is surprisingly simple: to create an outgoing link from an object o you must be a member of the permission group of o. For the purposes of this rule, setting a primitive value on o is considered creating an outgoing link.
Another way to say this is that creating a link (between two objects or between an object and a primitive value) requires membership in the permission group of the object from which the link originates. Remember that Metaweb properties can be master properties, which represent outgoing links (and primitive values), or reverse properties, which represent incoming links. So we can also state the access control rule in terms of properties:
In order to set a master property of o to some other object p, the user must be a member of the permission group of o.
In order to set a reverse property of o to some other object p, the user must be a member of the permission group of p.
The basic rule is very simple. But many important aspects of the Metaweb access control model flow from it. For example, /type/object/type is a master property (with /type/type/instance its reverse). This means that any user can link an object they create to any type, regardless of the permissions associated with the type. It is simply not possible to define a Metaweb type that other users cannot use.
Although the use of types is not subject to access control, the use of namespaces is tightly controlled. The /type/object/key property is a reverse property: /type/namespace/keys is the master property. This means that adding or modifying names in a namespace requires membership in the the permission group of the namespace. This means, for example, that users can't add types to other user's domains (unless the owner of that domain has added them to an appropriate usergroup), can't modify the system types in the /type domain, and can't define new languages in the /lang namespace.
Metaweb allows one additional layer of access control on top of the basic access control rule. It is possible to define properties that require special permission to set. Per-property access control is not based on the permission groups of the objects being linked, but on the permission group of the type that defines the property. This feature is important to owners of authoritative datasets who want to make that data available on a read-only basis through properties on existing objects.
At the time of this writing [23], the freebase.com client does not allow you to define properties of this kind. Instead, you must use a MQL write query to manually set the /type/property/requires_permission to true for the property. As an example, let's make the next property of our Note type require per-property permissions:
| Write | Result |
|---|---|
{
"id":"/user/docs/music/note/next",
"/type/property/requires_permission":{
"connect":"update",
"value":true
}
}
|
{
"id" : "/user/docs/music/note/next",
"/type/property/requires_permission" : {
"connect" : "inserted",
"value" : true
}
}
|
With this change made, setting the next property requires membership in the permission group of the Note type (which, by default, is the same as the permission group of the /user/docs/music domain). Anyone can create Note instances, but only owners of the type can set the next property. Note that per-property access control is in addition to, not instead of, per-object access control. So to set the next property of a Note object, the user must be a member of the permission group of the object itself, and also be a member of the permission group of the Note type.
[22] Remember, however, that Metaweb maintains a modification history for each object. We learned how to query the history of an object and the historical state of an object in Section 3.7.4 and Section 4.2.4.4. The freebase.com client also makes object history available through links on the pages it displays.
[23] September, 2008