        ;; invalid sort
        :else (throw (RuntimeException. "invalid sort specified in query"))))
     query-object))
+
+
+(defn query [& {:keys [kind ancestor filter sort keys-only
+                       count-only? in-transaction?
+                       limit offset
+                       start-cursor end-cursor ; TODO: Implement this.
+                       prefetch-size chunk-size]
+                :or {keys-only false, filter [[]], sort [[]],
+                     count-only? false, in-transaction? false}}]
+  (let [query-object (make-query-object kind ancestor filter sort keys-only)
+        fetch-options-object (FetchOptions$Builder/withDefaults)]
+    (when limit
+      (.limit fetch-options-object limit))
+    (when offset
+      (.offset fetch-options-object offset))
+    (when prefetch-size
+      (.prefetchSize prefetch-size))
+    (when chunk-size
+      (.chunkSize chunk-size))
+    (let [prepared-query (if (and in-transaction? *current-transaction*)
+                             (.prepare (get-datastore-service) *current-transaction*
+                                       query-object)
+                             (.prepare (get-datastore-service) query-object))]
+      (if count-only?
+          (.countEntities prepared-query)
+          (seq (.asIterable prepared-query fetch-options-object))))))

         (save!-helper this# ~kind :parent parent#)))))


;;; Note that the code relies on the API's implicit transaction tracking, and
;;; the *current-transaction* value is not used. Making it available just in
;;; case.
(defmacro with-transaction [& body]
  `(binding [*current-transaction* (.beginTransaction (get-datastore-service))]
     (try

             (throw err#))))))


(defn- make-query-object [kind ancestor filter sort keys-only]
  (let [kind (cond (nil? kind) kind
                   (string? kind) kind
                   (extends? EntityProtocol kind) (unqualified-name kind)
