Querying/Parsing JSON in Swift 3 query

Hi jens, your explanation was very helpful and I think I got most of this now.

The reason why I wanted this in a database was because I’ll be adding countries/states to this list in the future. However, I do see the value of uniquely identifying that document and just referencing it directly and not through a query. By well known ID, you mean something like “62023fc0264ac81be77e3f1215873c16” correct? Those doc ID’s are impossible to memorize, lol.

Here’s how I set it all up (for those newly to CBL that have the same questions as mine).

  1. Created the view (in AppDelegate after opening the database):

    let countryView = database.viewNamed(“countrylist”)
    countryView.setMapBlock({ (doc, emit) in
    if let type = doc[“type”] as? String, let statelist = doc[“statelist”], let country = doc[“country”], type == “location_array” {
    emit(country,[statelist])
    }
    }, version: “13”)

  2. Set up a begin and end key:

    let beginCountry = "Japan"
    let endCountry = “USA”

  3. invoked the query:

    let query = database.viewNamed(“countrylist”).createQuery()
    query.limit = 20
    query.startKey = beginCountry
    query.endKey = endCountry

    do { let result = try query.run()
    while let row = result.nextRow() {
    let resultValue = (row.value as! NSArray) as Array
    let countryValue = row.key
    let stateValue = resultValue[0]
    locationData.append(locationStruc(country: countryValue as! String, statelist: stateValue as! [String]))
    }
    } catch { return }

I was able to then populate the pickerview with the countries between Japan and USA (inclusive). In all likelihood, I’ll remove the begin and end criteria for this view and it was more for learning purpose. This is really neat stuff and I hope to become more fluent with it.

I did run across a problem trying to run the query in a different view form. If I started with:

    let queryx = database.viewNamed("countrylist").createQuery()
    queryx.limit = 20
    queryx.startKey = "USA"
    queryx.endKey = "USA"

It complained that “database” was an unresolved identified. Thinking that since “query” was already set up correctly earlier, I just removed that and tried:

    query.limit = 20
    query.startKey = "USA"
    query.endKey = "USA"
  
    do { let result = try query.run()
      while let row = result.nextRow() {
        let resultValue = (row.value as! NSArray) as Array
        let countryValue = row.key
        let stateValue = resultValue[0]
        locationData.append(locationStruc(country: countryValue as! String, statelist: stateValue as! [String]))
      }
    } catch { return }

but it didn’t recognize “query” as a valid identifier.

Does the “database” have to be declared globally in AppDelegate so that other forms can use it? Is “database” considered a variable? Here’s how I opened the database:

  func openDatabase(username:String, withKey key:String?,
                    withNewKey newKey:String?) throws {
    // TRAINING: Create a database
    let dbname = username
    let options = CBLDatabaseOptions()
    options.create = true
    
    if kEncryptionEnabled {
      if let encryptionKey = key {
        options.encryptionKey = encryptionKey
      }
    }
    
    try database = CBLManager.sharedInstance().openDatabaseNamed(dbname, with: options)
    if newKey != nil {
      try database.changeEncryptionKey(newKey)
    }

Thanks,
David

You can create a document with any ID you want. Just access it as ’database[“foo”]`. If it there’s no document with that ID yet, you get an empty CBLDocument; storing it creates the document.

Your second question has to do with the scope of variables in Swift. From your code I can’t tell where database is declared; looks like it’s a property of the class that contains the openDatabase method. If so they you can use that variable in any method of that class. Sounds like the code where you’re creating queries is in a different class; if so, you’d have to get the database via the property of the app delegate. This is basic Swift stuff, so I suggest rereading the relevant parts of the Swift docs.

Hi jens, I think I have it now, thanks for the tip.

In an explanation above, you mentioned how to find a particular city within a state by saying:

“If you want to find all the cities in a state, you make a map function that goes through your data and emits the state as the key and the city as the value. Then you query it with startKey and endKey set to the state you want.”

I think that would work if no country has the same name of a state that is also in a different country. In other words, every state across all countries is uniquely named. So if a user selects “IL” as the state, it would return all cities in Illinois, and hopefully no other country had a state named Illinois. I’d like to plan for the case where it could happen and in the RDBMS world I would just add more to the “where” clause. How would I do that in the above? It seems like I would have perhaps three keys: startKey1 and endKey1 = “USA”, and startKey2 and endKey2 = “IL”, and startKey3 = “Chicago” and endKey3 = “Chicago” if I just wanted to narrow it just one city.

Hopefully I’m making sense here.

Thanks again,
David

You would emit an array [country, city] as the key. Please read the section of the docs on compound keys.

Hi jens/Priya,

I have a follow up question on the setMapBlock syntax, which I haven’t been able to find an answer to.

If I have a set of documents like the following:

{
“country”: “USA”,
“state”: “IL”,
“addressInfo”: {
“name”: “John Doe”,
“city”: “myCity”,
“street”: “myStreet Avenue”,
“zip”: “12345”
}
}

And I wanted to find all documents for people who lived on “myStreet Avenue”, how do I format that in the setMapBlock definition?

I tried something like “… let streetname = doc[“addressInfo:street”],…” but that didn’t seem to work, along with some other variations of that.

Basically, I’m trying to find documents that have certain subvalues, if that makes sense.

So far, I’ve been able to successfully get this going and I thank you for all your help above.

Thanks,
David

doc is always a dictionary of [String:AnyObject]. In the case of the addressInfo key, the associated object is itself a [String:AnyObject] because that’s what JSON objects turn into. So you need to cast it to that type before you can index into it. My Swift is very rusty, but it’s something like

let addressInfo = doc["addressInfo"] as? [String:AnyObject]
let street = addressInfo?["street"]

Your idea of ... let streetname = doc["addressInfo:street"],... is actually a lot like the new KeyPath feature in Swift 4, but the syntax is different.

Hi jens, yes, that makes sense and I had been using that. I was more asking how to do that in the creation of a view such as:

let countryView = database.viewNamed(“countrylist”)
countryView.setMapBlock({ (doc, emit) in
if let type = doc[“type”] as? String, let statelist = doc[“statelist”], let country = doc[“country”], type == “location_array” {
emit(country,[statelist])
}
}, version: “13”)

In the above example, finding country is straightforward because the document is flat (country and statelist are at the same level). My particular question was how to structure a view where the key is at a sub-element, and I apologize for not using the right terminology. I wish my example document of the address had indentation as it would be a little more clear.

Thanks,
David

Edit: I might have read your answer too quickly. Perhaps I can place your two “let” examples within the setMapBlock statement, separated with commas?

Yep, that worked.

Thanks again.

David

I’m not sure what you’re asking. You were having trouble accessing nested properties, so I showed an example. It works the same way whether or not you’re using it in a map block or not.

Perhaps I can place your two “let” examples within the setMapBlock statement, separated with commas?

Um, you can have as many statements/lines as you want in any block, including a map block.

Hi jens, following up on the first part of one of your replies.

You had mentioned creating a document with a well known id and I’ve been struggling a little bit with that. I wanted to create them using the Public REST API function (database POST bulk docs) using something like the following as a test document:

{
“docs”:
[
{
“id”: “A101_01”,
“rev”: “1-A101_01”,
“ok”: true,
“country”: “USA”,
“state”: “GA”,
“city”: “Atlanta”,
“region”: “Atlanta Area”,
“creation_date”: “7/19/2017”,
“valid”: “yes”,
“note”: “mynote”,
“rectype”: “city”
}
],
“new edits”: true
}

However, when I try this, along with a number of syntax variations, I keep getting the 409 error, which states something along the lines that the document already exists but a revision number wasn’t specified. I’m a little confused as to what I might be doing wrong in the above.

Thanks,
David

409 implies that a document already exists with the specified Id. Do a GET to check if the document already exists.
When creating a doc, you shouldn’t specify the rev. That’s generated when the doc is first created. Subsequently, when you want to update a revision, you specify the rev

Hi Priya,

Ok, so I removed the rev number above and then used “A999_01” as the id and tried that combination. I still got the 409 error. I then took A999_01 and put that in the GET function and was told that the document does not exist (error 404).

I also tried a variation using “_id”: instead of “id”: and that made no difference.

If you took this snippet and tried inserting it into one of your test databases using the POST bulk docs function, do you get the same error?

Thanks,
David

I tested your request and works fine. Just have to replace “_id” with “id”.
What does your sync function do? It is perhaps doing some validation that is preventing the request.
Try with a default sync function.

FYI:
POST {{adminurl}}/{{db}}/_bulk_docs

{
"docs":
[
{
"_id": "A101_01",
"ok": true,
"country": "USA",
"state": "GA",
"city": "Atlanta",
"region": "Atlanta Area",
"creation_date": "7/19/2017",
"valid": "yes",
"note": "mynote",
"rectype": "city"
}
],
"new edits": true
}

Response:

[
    {
        "id": "A101_01",
        "rev": "1-80cb514f3b685983b5ca90bf5e1d7016"
    }
]

Is this on Sync Gateway now? I’m confused because all your previous questions were on the Couchbase Lite side.

Also, _bulk_docs is overkill if you only want to create one document. Just do a PUT to the doc’s URL.

Hi jens,

That was just a test document. In the end, I’m going to create a few hundred of them all at once.

David

Yes, that particular question was referring to the sync gateway. It kind of spawned out of the reference to me creating my own well-known id so I don’t have to run a query when I’m looking for something specific and static.

Hi Priya, that variation worked. I think I was confused because the 409 error message on that page is just an example and not necessarily the actual response. I thought it was the actual response after I hit the “Try it out!” button. I suspect a few of my prior attempts actually worked and at some point will find and remove those documents.

Thanks a bunch.

OK. Glad its sorted! Also, in future, it would help to start a separate thread for a new problem - otherwise gets pretty confusing !