Commits

Liam Staskawicz committed d668db8

* work on limit and skip in cursor, w/test even

Comments (0)

Files changed (3)

   private static const Log log := Log.get("mongo")
   internal const Collection coll
   const Map selector            // document selector
-  const Map opts                // query options
-  private Bool queryStarted := false // have we opened up this cursor via query() yet?
+  Map opts                      // query options
+  private Int itemsSeen := -1   // how many items have we seen? -1 indicates we haven't even queried
   private Int cursorID := 0     // DB assigned cursor ID
   private List cache := [,]     // cache of returned objects
   private Bool closed := false  // set to true when the DB tells us there's nothing left
   
   **
   ** Get the next element from this cursor.
+  ** Returns null if no more elements are available.
   **
   Map? next()
   {
   private Void fillErUp()
   {
     if (!closed && cache.size == 0) {
-      if(queryStarted == true)
-        getMore()
-      else
-        doQuery()
+      if(itemsSeen < 0) { // we haven't sent our initial query yet
+        itemsSeen = 0
+        doQuery(numToReturn())
+      }
+      else {
+        num := numToReturn()
+        if(num < opts.get("limit", num) && num > 0)
+          getMore(num)
+      }
     }
   }
   
+  // a return value less than zero indicates that we shouldn't even try to fetch anything
+  private Int numToReturn()
+  {
+    num := opts.get("batchsize", 0) // default to 0 which lets the DB decide how much to send back
+    if(opts.containsKey("limit")) {
+      num = ((Int)opts["limit"] - itemsSeen)
+      if(num == 0)
+        num = -1
+    }
+    return num
+  }
+  
+  **
+  ** Set the limit for the number of results returned by this cursor.
+  ** If it has already returned more than the limit passed in,
+  ** this has no effect.
+  **
   This limit(Int lim)
   {
-    opts["limit"] = lim
+    if(lim > itemsSeen)
+      opts["limit"] = lim
     return this
   }
   
-  This skip(Int c)
+  **
+  ** Skip to an offset into the cursor.
+  ** Note that this is only meaningful before the cursor has retrieved
+  ** any documents (once more() or next() have been called).  
+  **
+  This skip(Int offset)
   {
-    opts["skip"] = c
+    opts["skip"] = offset
     return this
   }
   
+  **
+  ** The count of items available from this cursor.
+  ** Note that this does not take into account any items that
+  ** have already been retrieved by this cursor - ie, count will not
+  ** decrease as you call next().
+  ** It does, however, take into account limit() and skip()
+  **
   Int count()
   {
     cmd := Str:Obj[:] { ordered = true }
     res := coll.db.command(cmd)
     if (DB.cmdOk(res)) {
       c := (res["n"] as Float).toInt
-      return c.min(opts.get("limit", c))
+      return c.min(opts.get("limit", c)) - opts.get("skip", 0)
     } 
-    if (res["errmsg"] == "ns missing")
+    else if (res["errmsg"] == "ns missing")
       return 0 
-    throw MongoOpErr("count() failed - ${res}")
+    else
+      throw MongoOpErr("count() failed - $res")
   }
   
   // This sort()
   private Str fullName()
   {
     return (opts["admin"] == true) ? "admin.${coll.name}" : coll.fullName
-  } 
+  }
   
-  private Void doQuery()
+  private Void doQuery(Int numToRetrieve)
   {
     b := Buf() { endian = Endian.little }
     b.writeI4(queryOpts)                        // query opts
     Bson.writeCStr(b.out, fullName)             // full name
     b.writeI4(opts.get("skip", 0))              // skip
-    b.writeI4(opts.get("batchsize", 0))         // num to return
+    b.writeI4(numToRetrieve)                    // num to return
     Bson.write(b.out, selector)                 // query object
     if(opts.containsKey("fields"))              // optional fieldReturnSelector
       Bson.write(b.out, opts["fields"])
     s := coll.db.connection.getSocket()
     reqID := coll.db.connection.sendMsg(s.out, b.flip, MongoOp.QUERY)
     readResponse(s.in, reqID)
-    this.queryStarted = true
   }
   
-  private Void getMore()
+  private Void getMore(Int numToRetrieve)
   {
     b := Buf() { endian = Endian.little }
     b.writeI4(0)                          // reserved
     Bson.writeCStr(b.out, fullName)       // full name
-    b.writeI4(opts.get("batchsize", 0))   // num to return
+    b.writeI4(numToRetrieve)              // num to return
     b.writeI8(this.cursorID)              // cursor ID
     
     s := coll.db.connection.getSocket()
       close
     startingFrom := ins.readS4()
     numberReturned := ins.readS4()
+    itemsSeen += numberReturned
     // Sys.out.printLine("cursorID - ${cursorID}, startingFrom - ${startingFrom}, numberReturned - ${numberReturned}")
     
     numberReturned.times { cache.add(Bson.read(ins)) }

test/CursorTest.fan

+
+
+
+
+**
+**  CursorTest
+**
+class CursorTest : Test
+{
+  Mongo mongo := Mongo()
+  DB db := mongo.db("curses")
+  
+  override Void setup()
+  {
+  
+  }
+
+  override Void teardown()
+  {
+  
+  }
+  
+  Void testLimit()
+  {
+    coll := db["limtest"]
+    coll.drop
+    verifyEq(0, coll.find().count)
+    i := 0
+    10.times { coll.insert(["limmmmm":i++]) }
+    verifyEq(10, coll.find().count)
+    verifyEq(3, coll.find().limit(3).count)
+    
+    // test passing in a limit that is less than the number already returned.
+    // should have no effect - we should just get all the elements out of the cursor
+    curs := coll.find()
+    5.times { curs.next }
+    curs.limit(3)
+    verify(curs.more)
+    5.times { curs.next }
+    verifyFalse(curs.more)
+    
+    // test passing it to the cursor ctor
+    curs = coll.find([:], ["limit":3])
+    verifyEq(3, curs.count)
+    3.times { curs.next }
+    verifyFalse(curs.more)
+    coll.drop
+  }
+  
+  Void testSkip()
+  {
+    coll := db["skiptest"]
+    coll.drop
+    verifyEq(0, coll.find().count)
+    
+    i := 0
+    20.times { coll.insert(["skipppppppppppppppp":i++]) }
+    verifyEq(20, coll.find().count)
+    verifyEq(10, coll.find().skip(10).count)
+    cursor := coll.find([:], ["skip":10])
+    verifyEq(10, cursor.count)
+    10.times { cursor.next }
+    verifyFalse(cursor.more)
+    coll.drop
+  }
+  
+  Void testToList()
+  {
+    coll := db["skiptest"]
+    coll.drop
+    verifyEq(0, coll.find().count)
+    i := 0
+    iter := 20
+    iter.times { coll.insert(["v":i++]) }
+    list := coll.find().toList
+    verifyEq(iter, list.size)
+    list.each |Str:Obj? val, j| {
+      if(val["v"] != j)
+        fail("cursor.toList() returned different values than expected")
+    }
+    coll.drop
+  }
+
+}
     db.setProfilingLevel(proflevel) // reset it
     
     s := db.profilingInfo
-    Sys.out.printLine("s - ${s}")
+    Sys.out.printLine("profilingInfo - ${s}")
   }
   
   Void testAuthentication()
   {
     db.removeUser(testuser)
-    verify(!db.users().contains(testuser))
+    verifyFalse(db.users().contains(testuser))
     db.addUser(testuser, testpass)
     verify(db.users().contains(testuser))
     verify(db.authenticate(testuser, testpass))
   {
     newcoll := "newcoll"
     fullname := "${db.name}.${newcoll}"
-    verify(!db.collectionNames().contains(fullname))
+    verifyFalse(db.collectionNames().contains(fullname))
     db[newcoll].insert(["rando":"tester"])
     verify(db.collectionNames().contains(fullname))
     db.dropCollection(newcoll)
-    verify(!db.collectionNames().contains(fullname))
+    verifyFalse(db.collectionNames().contains(fullname))
   }
   
 }