[Pkg-javascript-commits] [node-levelup] 01/02: Imported Upstream version 0.18.5

Andrew Kelley andrewrk-guest at moszumanska.debian.org
Mon Jun 30 19:20:20 UTC 2014


This is an automated email from the git hooks/post-receive script.

andrewrk-guest pushed a commit to branch master
in repository node-levelup.

commit 8ddb1c0a915962a2fc69249d85281788c9b80235
Author: Andrew Kelley <superjoe30 at gmail.com>
Date:   Mon Jun 30 19:11:00 2014 +0000

    Imported Upstream version 0.18.5
---
 .dntrc                                             |  19 +
 .gitignore                                         |   9 +
 .jshintrc                                          |  59 ++
 .npmignore                                         |   6 +
 .travis.yml                                        |  23 +
 CHANGELOG.md                                       | 174 +++++
 CONTRIBUTING.md                                    |  27 +
 LICENSE                                            |  39 ++
 README.md                                          | 732 +++++++++++++++++++++
 buster.js                                          |   7 +
 lib/batch.js                                       |  78 +++
 lib/errors.js                                      |  22 +
 lib/levelup.js                                     | 435 ++++++++++++
 lib/read-stream.js                                 | 126 ++++
 lib/util.js                                        | 182 +++++
 lib/write-stream.js                                | 178 +++++
 package.json                                       |  73 ++
 test/approximate-size-test.js                      |  80 +++
 test/argument-checking-test.js                     | 122 ++++
 test/batch-test.js                                 | 372 +++++++++++
 test/benchmarks/README.md                          |  42 ++
 test/benchmarks/engines/index.js                   |  19 +
 test/benchmarks/engines/leveled.js                 |  21 +
 test/benchmarks/engines/levelup-nosnappy.js        |  21 +
 test/benchmarks/engines/levelup-release.js         |  21 +
 test/benchmarks/engines/levelup.js                 |  21 +
 test/benchmarks/engines/sqlite3.js                 |  26 +
 test/benchmarks/index.js                           |  97 +++
 test/benchmarks/package.json                       |  16 +
 test/benchmarks/stream-bench.js                    |  43 ++
 .../tests/batch_int_string_x100000_leveled.js      |   1 +
 .../tests/batch_int_string_x100000_levelup.js      |   1 +
 .../tests/batch_int_string_x1000_leveled.js        |  20 +
 .../tests/batch_int_string_x1000_levelup.js        |  21 +
 .../tests/get_int_string_x1000_leveled.js          |  14 +
 .../tests/get_int_string_x1000_levelup.js          |  29 +
 .../tests/get_int_string_x1000_sqlite3.js          |  33 +
 .../benchmarks/tests/get_int_string_x10_leveled.js |   2 +
 .../benchmarks/tests/get_int_string_x10_levelup.js |   2 +
 .../benchmarks/tests/get_int_string_x10_sqlite3.js |   2 +
 test/benchmarks/tests/index.js                     |  94 +++
 .../tests/put_int_string_x100000_leveled.js        |   1 +
 .../tests/put_int_string_x100000_levelup.js        |   1 +
 .../tests/put_int_string_x100000_sqlite3.js        |   1 +
 .../tests/put_int_string_x1000_leveled.js          |   1 +
 .../tests/put_int_string_x1000_levelup.js          |  20 +
 .../tests/put_int_string_x1000_sqlite3.js          |  22 +
 .../benchmarks/tests/put_int_string_x10_leveled.js |   1 +
 .../benchmarks/tests/put_int_string_x10_levelup.js |   1 +
 .../benchmarks/tests/put_int_string_x10_sqlite3.js |   1 +
 .../benchmarks/tests/readStream_x100000_levelup.js |  17 +
 test/benchmarks/tests/readStream_x10000_levelup.js |   3 +
 test/benchmarks/tests/readStream_x1000_levelup.js  |   3 +
 test/benchmarks/tests/readStream_x100_levelup.js   |   3 +
 test/benchmarks/tests/readStream_x10_levelup.js    |   3 +
 test/binary-test.js                                | 170 +++++
 test/common.js                                     | 156 +++++
 test/compression-test.js                           |  90 +++
 test/copy-test.js                                  |  68 ++
 test/create-stream-vs-put-racecondition.js         | 102 +++
 test/data/testdata.bin                             | Bin 0 -> 4688 bytes
 test/deferred-open-test.js                         | 192 ++++++
 test/destroy-repair-test.js                        |  81 +++
 test/encoding-test.js                              | 160 +++++
 test/functional/binary-data-test.js                |  43 ++
 test/functional/compat-test.js                     |  50 ++
 test/functional/fstream-test.js                    | 102 +++
 test/functional/tarcommon.js                       |  97 +++
 test/functional/test-data.db.tar                   | Bin 0 -> 3153920 bytes
 test/functional/test-data.tar                      | Bin 0 -> 2273280 bytes
 test/get-put-del-test.js                           | 185 ++++++
 test/idempotent-test.js                            |  57 ++
 test/init-test.js                                  | 217 ++++++
 test/inject-encoding-test.js                       | 104 +++
 test/json-test.js                                  |  93 +++
 test/key-value-streams-test.js                     | 109 +++
 test/leveldown-substitution-test.js                |  56 ++
 test/null-and-undefined-test.js                    | 133 ++++
 test/open-patchsafe-test.js                        |  81 +++
 test/optional-leveldown-test.js                    |  72 ++
 test/read-stream-test.js                           | 685 +++++++++++++++++++
 test/snapshot-test.js                              |  63 ++
 test/test-10k-times.sh                             |  12 +
 test/write-stream-test.js                          | 467 +++++++++++++
 84 files changed, 7032 insertions(+)

diff --git a/.dntrc b/.dntrc
new file mode 100644
index 0000000..1cc9d2d
--- /dev/null
+++ b/.dntrc
@@ -0,0 +1,19 @@
+## DNT config file
+## see https://github.com/rvagg/dnt
+
+NODE_VERSIONS="\
+  master   \
+  v0.11.9  \
+  v0.11.8  \
+  v0.10.22 \
+  v0.10.21 \
+  v0.8.26  \
+"
+OUTPUT_PREFIX="levelup-"
+TEST_CMD="\
+  cd /dnt/ &&                                                    \
+  rm -rf node_modules/leveldown/ &&                              \
+  npm install --nodedir=/usr/src/node &&                         \
+  node_modules/.bin/tap test/*-test.js --stderr;                 \
+#"
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a4ca25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+build/
+libleveldb.so
+libleveldb.a
+test-data/
+_benchdb_*
+*.sw*
+bulk2ssd_pop.config
+libpeerconnection.log
\ No newline at end of file
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..7937650
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,59 @@
+{
+    "predef": [ ]
+  , "bitwise": false
+  , "camelcase": false
+  , "curly": false
+  , "eqeqeq": false
+  , "forin": false
+  , "immed": false
+  , "latedef": false
+  , "noarg": true
+  , "noempty": true
+  , "nonew": true
+  , "plusplus": false
+  , "quotmark": true
+  , "regexp": false
+  , "undef": true
+  , "unused": true
+  , "strict": false
+  , "trailing": true
+  , "maxlen": 120
+  , "asi": true
+  , "boss": true
+  , "debug": true
+  , "eqnull": true
+  , "esnext": true
+  , "evil": true
+  , "expr": true
+  , "funcscope": false
+  , "globalstrict": false
+  , "iterator": false
+  , "lastsemic": true
+  , "laxbreak": true
+  , "laxcomma": true
+  , "loopfunc": true
+  , "multistr": false
+  , "onecase": false
+  , "proto": false
+  , "regexdash": false
+  , "scripturl": true
+  , "smarttabs": false
+  , "shadow": false
+  , "sub": true
+  , "supernew": false
+  , "validthis": true
+  , "browser": true
+  , "couch": false
+  , "devel": false
+  , "dojo": false
+  , "mootools": false
+  , "node": true
+  , "nonstandard": true
+  , "prototypejs": false
+  , "rhino": false
+  , "worker": true
+  , "wsh": false
+  , "nomen": false
+  , "onevar": true
+  , "passfail": false
+}
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..e063178
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,6 @@
+*.tar.gz
+build/
+test-data.tar
+test-data.db.tar
+test/benchmarks/
+level/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7a0a4d0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: node_js
+node_js:
+  - "0.8"
+  - "0.10"
+branches:
+  only:
+    - master
+notifications:
+  email:
+    - r at va.gg
+    - john at chesl.es
+    - raynos2 at gmail.com
+    - dominic.tarr at gmail.com
+    - max at maxogden.com
+    - lars.magnus.skog at gmail.com
+    - david.bjorklund at gmail.com
+    - julian at juliangruber.com
+    - paolo at async.ly
+    - anton.whalley at nearform.com
+    - matteo.collina at gmail.com
+    - pedro.teixeira at gmail.com
+    - mail at substack.net
+script: npm run-script alltests
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4b7f8a7
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,174 @@
+### 0.18.1 @ Nov 20 2013
+ * Make chained-batch obey global LevelUP object options (@mcavage)
+
+### 0.18.0 @ Nov 18 2013
+
+ * Upgrade to LevelDOWN at 0.10.0 (and bops at 0.1.0 and readable-stream at 1.1.9) (@rvagg)
+
+### 0.17.0 @ Oct 01 2013
+
+ * Undo factory pattern, use plain prototypal object and expose full prototype (@rvagg)
+ * Move Batch object to batch.js and expose (@rvagg)
+ * Use new package, DeferredLevelDOWN to handle all deferred open logic (@rvagg)
+ * Code cleanup, update deps (xtend) (@rvagg, @juliangruber)
+
+### 0.16.0 @ Sep 10 2013
+
+ * Added `notFound` boolean property and `status=404` property to NotFoundError (@rvagg)
+ * Upgrade to errno at 0.1.0 which aliases .type and .name properties (@rvagg)
+ * ReadStream gracefully handles multiple destroy() calls (@mcollina)
+
+### 0.15.0 @ Aug 25 2013
+
+ * New ReadStream: upgrade to streams2, remove all state-management cruft, remove fstream support (@substack)
+ * Update LevelDOWN dependency to ~0.8.0 with Iterator lt/lte/gt/gte support and NAN as a dependency
+ * Added @substack as contributor
+
+### 0.14.0 @ Aug 19 2013
+
+ * Encodings overhaul, allow custom encoders/decoders for `keyEncoding` or `valueEncoding` (@dominictarr)
+
+### 0.13.0 @ Aug 11 2013
+
+ * Update LevelDOWN dependency version ~0.7.0 for Node 0.8->0.11 compatibility
+
+### 0.12.0 @ Jul 25 2013
+
+  * Update LevelDOWN dependency version ~0.6.2
+
+### 0.11.0 @ Jul 17 2013
+
+  * Remove all Function#bind calls for better browser compatibility (@juliangruber)
+  * Switch from direct Buffer access to bops for better browser compatibility (@juliangruber)
+  * WriteStream#end accepts `data` argument (@pgte)
+  * Added @pgte as contributor
+
+### 0.10.0 @ Jun 14 2013
+
+  * Upgrade to LevelDOWN at 0.6.0 which upgrades to LevelDB at 1.11.0, some important bugfixes: https://groups.google.com/forum/#!topic/leveldb/vS1JvmGlp4E
+
+### 0.9.0 @ 21 May 2013
+
+  * Use LevelDOWN at 0.5.0, see https://github.com/rvagg/node-leveldown/blob/master/CHANGELOG.md for details
+  * Race-condition(ish) fixed in ReadStream--createReadStream() does not start immediately and therefore allowed put()s to happen before the stream starts (@dominictarr)
+  * ReadStream doesn't emit "ready" event (@dominictarr)
+  * Allow separate encodings per operation in db.batch() (@juliangruber)
+  * Allow separate encodings per write() in WriteStream (@juliangruber)
+  * WriteStream supports "type" option ("put" [default] or "del") on constructor and individual write()s (@mcollina)
+  * Remove "leveldown" from dependencies (see http://r.va.gg/2013/05/levelup-v0.9-some-major-changes.html) (@rvagg)
+  * Expose LevelDOWN (or LevelDOWN substitute) as `db` property on LevelUP instance (e.g. db.db.approximateSize())
+  * Chained batch exposed from LevelDOWN, invoked with argument-less db.batch() (@juliangruber, @rvagg)
+  * Significantly improve ReadStream performance by replacing .bind() and .apply() (@mcollina, @kesla)
+  * Better Browserify support (@rvagg, @juliangruber, @maxogden, etc.)
+  * Deprecate secondary LevelDB-specific operations on LevelUP, prefer direct LevelDOWN access (approximateSize(), repair(), destroy(), getProperty()--new in LevelDOWN at 0.5.0) (@rvagg)
+
+### 0.8.0 @ 17 Apr 2013
+
+  * More comprehensive argument checking, will now report back directly or throw if there is a problem rather than on nextTick (@rvagg)
+  * Expose `.options` property on LevelUP instances. (@rvagg)
+  * Further clarify 'encoding' -> 'valueEncoding' shift. db.options.valueEncoding is now authoritative even if user used 'encoding' on initialisation. (@rvagg)
+  * 'level' package now published to npm that bundles LevelUP & LevelDOWN and exposes LevelUP directly; for planned shift to detaching LevelDOWN as a direct-dependency of LevelUP. (@rvagg)
+
+### 0.7.0 @ 8 Apr 2013
+
+  * Windows support in LevelDOWN @0.2.0 (@rvagg)
+  * added 'db' option on constructor to replace LevelDOWN (@rvagg)
+  * added repair() & destroy() aliases for LevelDOWN implementations (@rvagg)
+  * fix early 'close' emit in WriteStream (@rvagg)
+  * improved ReadStream reverse=true start key handling (@kesla)
+  * ReadStream empty start & end keys ignored rather than segfault (@kesla)
+  * 'encoding' option now an alias for 'valueEncoding' only, 'keyEncoding' defaults to 'utf8' and must be changed explicitly (@rvagg)
+
+### 0.6.2 @ 4 Mar 2013
+
+  * use `xtend` package instead of internal util._extend @ralphtheninja
+  * internal cleanup of `callback` argument detection @ralphtheninja
+  * move deferred-open-operations into an internal `this._db` wrapper rather than make them call public .get()/.put() etc. for a second time @dominictarr
+
+### 0.6.1 @ 1 Mar 2013
+
+  * internal code cleanup & refactoring @ralphtheninja
+  * fix multiple iterator.end() calls in ReadStreams throwing errors (destroy() called while read/next is in progress) #82 #83 #84 @rvagg
+
+### 0.6.0 @ Feb 25 2013
+
+  * complete transition to LevelDOWN for the LevelDB binding. No native code left in LevelUP @rvagg
+    - LevelDOWN now keeps its own ChangeLog at: https://github.com/rvagg/node-leveldown/blob/master/CHANGELOG.md
+    - LevelDB at 1.9.0 and Snappy at 1.1.0 are included in LevelDOWN at 0.1.2
+  * simplify callback signature (remove extra, undocumented properties from some callbacks) @rvagg / @dominictarr
+
+### 0.5.4 @ Feb 16 2013
+
+  * explicit namespaces in C++ @rvagg
+  * memory leak, Persistent<Function> callback not Dispose()d for `readStream()` @rvagg
+  * allow one next() at a time, improve end() handling @rvagg
+  * ensure iterator end & next don't conflict @rvagg
+  * remove CloseError @ralphtheninja
+  * fix put/batch bug in WriteStream#_process() @ralphtheninja
+  * remove `useBatch` in `copy()` @rvagg
+  * move encodingOpts levelup.js -> util.js @ralphtheninja
+
+### 0.5.3-1 @ Feb 5 2013
+
+  * non-shrinkwrapped release @rvagg
+
+### 0.5.3 @ Jan 28 2013
+
+  * `location` exposed as read-only property on db object @rvagg
+  * swap bufferstream dependency for simple-bufferstream, remove unnecessary compile @rvagg
+  * comment out all sqlite3 benchmarks @ralphtheninja
+  * put LevelUP() into closure @ralphtheninja
+
+### 0.5.2 @ Jan 24 2013
+
+  * fix: incorrect scope in approximateSize function @sandfox
+
+### 0.5.1 @ Jan 10 2013
+
+  * change `createIfMissing` option default to `true` @rvagg
+  * use util._extend instead of local variant @rvagg
+  * adjust copyright & contributors @rvagg
+  * idempotent open and close, and emit _state as events @dominictarr
+  * fix: check that UINT32_OPTION_VALUE is a Uint32 @kesla
+  * feature: Support setting size of LRU-cache @kesla
+  * use util.inherits() from node core @ralphtheninja
+
+### 0.4.4 @ Jan 1 2013
+
+  * set maxListeners to Infinity to prevent warnings when using deferred open @juliangruber
+
+### 0.4.3 @ Dec 31 2012
+
+  * added @kesla to contributors list @rvagg
+  * feature: added approximateSize() @kesla
+
+### 0.4.2 @ Dec 30 2012
+
+  * process.nextTick->setImmediate with polyfill Node 0.9.5 compat @rvagg
+  * added @ralphtheninja to contributors list @rvagg
+
+### 0.4.1 @ Dec 20 2013
+
+  * remove `useBatch` option on `writeStream()` @rvagg
+
+### 0.4.0 @ Dec 18 2013
+
+  * remove old, unused util functions @rvagg
+  * speed up batch() & allow non-Strings to C++ @rvagg
+  * fix batch() benchmarks @rvagg
+  * improved compression test @rvagg
+  * added SQLite3 to test suite @rvagg
+  * remove compile warnings on osx @rvagg
+  * return Strings not Buffers from C++ when possible @rvagg
+  * optimised encoders & decoders @rvagg
+  * added basic get() benchmarks @rvagg
+  * revamped benchmark suite @rvagg
+  * allow JS Strings through to native layer @rvagg
+  * cleaner build for osx @rvagg
+  * remove compile warnings for solaris @rvagg
+  * LevelDB 1.7 @rvagg
+  * added `compress` boolean on open() @rvagg
+
+### 0.3.x and prior
+
+  * stuff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2641fd0
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+# LevelUP is an OPEN Open Source Project
+
+-----------------------------------------
+
+## What?
+
+Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
+
+## Rules
+
+There are a few basic ground-rules for contributors:
+
+1. **No `--force` pushes** or modifying the Git history in any way.
+1. **Non-master branches** ought to be used for ongoing work.
+1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors.
+1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor.
+1. Contributors should attempt to adhere to the prevailing code-style.
+
+## Releases
+
+Declaring formal releases remains the prerogative of the project maintainer.
+
+## Changes to this arrangement
+
+This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change.
+
+-----------------------------------------
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..76d07a0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,39 @@
+Copyright 2012, Rod Vagg (the "Original Author")
+All rights reserved.
+
+MIT +no-false-attribs License
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+Distributions of all or part of the Software intended to be used
+by the recipients as they would use the unmodified Software,
+containing modifications that substantially alter, remove, or
+disable functionality of the Software, outside of the documented
+configuration mechanisms provided by the Software, shall be
+modified such that the Original Author's bug reporting email
+addresses and urls are either replaced with the contact information
+of the parties responsible for the changes, or removed entirely.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+
+Except where noted, this license applies to any and all software
+programs and associated documentation files created by the
+Original Author, when distributed with the Software.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7c0a845
--- /dev/null
+++ b/README.md
@@ -0,0 +1,732 @@
+LevelUP
+=======
+
+![LevelDB Logo](https://0.gravatar.com/avatar/a498b122aecb7678490a38bb593cc12d)
+
+**Fast & simple storage - a Node.js-style LevelDB wrapper**
+
+[![Build Status](https://secure.travis-ci.org/rvagg/node-levelup.png)](http://travis-ci.org/rvagg/node-levelup)
+
+[![NPM](https://nodei.co/npm/levelup.png?stars&downloads)](https://nodei.co/npm/levelup/) [![NPM](https://nodei.co/npm-dl/levelup.png)](https://nodei.co/npm/levelup/)
+
+
+  * <a href="#intro">Introduction</a>
+  * <a href="#leveldown">Relationship to LevelDOWN</a>
+  * <a href="#platforms">Tested & supported platforms</a>
+  * <a href="#basic">Basic usage</a>
+  * <a href="#api">API</a>
+  * <a href="#events">Events</a>
+  * <a href="#json">JSON data</a>
+  * <a href="#custom_encodings">Custom encodings</a>
+  * <a href="#extending">Extending LevelUP</a>
+  * <a href="#multiproc">Multi-process access</a>
+  * <a href="#support">Getting support</a>
+  * <a href="#contributing">Contributing</a>
+  * <a href="#licence">Licence & copyright</a>
+
+<a name="intro"></a>
+Introduction
+------------
+
+**[LevelDB](http://code.google.com/p/leveldb/)** is a simple key/value data store built by Google, inspired by BigTable. It's used in Google Chrome and many other products. LevelDB supports arbitrary byte arrays as both keys and values, singular *get*, *put* and *delete* operations, *batched put and delete*, bi-directional iterators and simple compression using the very fast [Snappy](http://code.google.com/p/snappy/) algorithm.
+
+**LevelUP** aims to expose the features of LevelDB in a **Node.js-friendly way**. All standard `Buffer` encoding types are supported, as is a special JSON encoding. LevelDB's iterators are exposed as a Node.js-style **readable stream** a matching **writeable stream** converts writes to *batch* operations.
+
+LevelDB stores entries **sorted lexicographically by keys**. This makes LevelUP's <a href="#createReadStream"><code>ReadStream</code></a> interface a very powerful query mechanism.
+
+**LevelUP** is an **OPEN Open Source Project**, see the <a href="#contributing">Contributing</a> section to find out what this means.
+
+<a name="leveldown"></a>
+Relationship to LevelDOWN
+-------------------------
+
+LevelUP is designed to be backed by **[LevelDOWN](https://github.com/rvagg/node-leveldown/)** which provides a pure C++ binding to LevelDB and can be used as a stand-alone package if required.
+
+**As of version 0.9, LevelUP no longer requires LevelDOWN as a dependency so you must `npm install leveldown` when you install LevelUP.**
+
+LevelDOWN is now optional because LevelUP can be used with alternative backends, such as **[level.js](https://github.com/maxogden/level.js)** in the browser or [MemDOWN](https://github.com/rvagg/node-memdown) for a pure in-memory store.
+
+LevelUP will look for LevelDOWN and throw an error if it can't find it in its Node `require()` path. It will also tell you if the installed version of LevelDOWN is incompatible.
+
+**The [level](https://github.com/level/level) package is available as an alternative installation mechanism.** Install it instead to automatically get both LevelUP & LevelDOWN. It exposes LevelUP on its export (i.e. you can `var leveldb = require('level')`).
+
+
+<a name="platforms"></a>
+Tested & supported platforms
+----------------------------
+
+  * **Linux**: including ARM platforms such as Raspberry Pi *and Kindle!*
+  * **Mac OS**
+  * **Solaris**: including Joyent's SmartOS & Nodejitsu
+  * **Windows**: Node 0.10 and above only. See installation instructions for *node-gyp's* dependencies [here](https://github.com/TooTallNate/node-gyp#installation), you'll need these (free) components from Microsoft to compile and run any native Node add-on in Windows.
+
+<a name="basic"></a>
+Basic usage
+-----------
+
+First you need to install LevelUP!
+
+```sh
+$ npm install levelup leveldown
+```
+
+Or
+
+```sh
+$ npm install level
+```
+
+*(this second option requires you to use LevelUP by calling `var levelup = require('level')`)*
+
+
+All operations are asynchronous although they don't necessarily require a callback if you don't need to know when the operation was performed.
+
+```js
+var levelup = require('levelup')
+
+// 1) Create our database, supply location and options.
+//    This will create or open the underlying LevelDB store.
+var db = levelup('./mydb')
+
+// 2) put a key & value
+db.put('name', 'LevelUP', function (err) {
+  if (err) return console.log('Ooops!', err) // some kind of I/O error
+
+  // 3) fetch by key
+  db.get('name', function (err, value) {
+    if (err) return console.log('Ooops!', err) // likely the key was not found
+
+    // ta da!
+    console.log('name=' + value)
+  })
+})
+```
+
+<a name="api"></a>
+## API
+
+  * <a href="#ctor"><code><b>levelup()</b></code></a>
+  * <a href="#open"><code>db.<b>open()</b></code></a>
+  * <a href="#close"><code>db.<b>close()</b></code></a>
+  * <a href="#put"><code>db.<b>put()</b></code></a>
+  * <a href="#get"><code>db.<b>get()</b></code></a>
+  * <a href="#del"><code>db.<b>del()</b></code></a>
+  * <a href="#batch"><code>db.<b>batch()</b></code> *(array form)*</a>
+  * <a href="#batch_chained"><code>db.<b>batch()</b></code> *(chained form)*</a>
+  * <a href="#isOpen"><code>db.<b>isOpen()</b></code></a>
+  * <a href="#isClosed"><code>db.<b>isClosed()</b></code></a>
+  * <a href="#createReadStream"><code>db.<b>createReadStream()</b></code></a>
+  * <a href="#createKeyStream"><code>db.<b>createKeyStream()</b></code></a>
+  * <a href="#createValueStream"><code>db.<b>createValueStream()</b></code></a>
+  * <a href="#createWriteStream"><code>db.<b>createWriteStream()</b></code></a>
+
+### Special operations exposed by LevelDOWN
+
+  * <a href="#approximateSize"><code>db.db.<b>approximateSize()</b></code></a>
+  * <a href="#getProperty"><code>db.db.<b>getProperty()</b></code></a>
+  * <a href="#destroy"><code><b>leveldown.destroy()</b></code></a>
+  * <a href="#repair"><code><b>leveldown.repair()</b></code></a>
+
+
+--------------------------------------------------------
+<a name="ctor"></a>
+### levelup(location[, options[, callback]])
+### levelup(options[, callback ])
+### levelup(db[, callback ])
+<code>levelup()</code> is the main entry point for creating a new LevelUP instance and opening the underlying store with LevelDB.
+
+This function returns a new instance of LevelUP and will also initiate an <a href="#open"><code>open()</code></a> operation. Opening the database is an asynchronous operation which will trigger your callback if you provide one. The callback should take the form: `function (err, db) {}` where the `db` is the LevelUP instance. If you don't provide a callback, any read & write operations are simply queued internally until the database is fully opened.
+
+This leads to two alternative ways of managing a new LevelUP instance:
+
+```js
+levelup(location, options, function (err, db) {
+  if (err) throw err
+  db.get('foo', function (err, value) {
+    if (err) return console.log('foo does not exist')
+    console.log('got foo =', value)
+  })
+})
+
+// vs the equivalent:
+
+var db = levelup(location, options) // will throw if an error occurs
+db.get('foo', function (err, value) {
+  if (err) return console.log('foo does not exist')
+  console.log('got foo =', value)
+})
+```
+
+The `location` argument is available as a read-only property on the returned LevelUP instance.
+
+The `levelup(options, callback)` form (with optional `callback`) is only available where you provide a valid `'db'` property on the options object (see below). Only for back-ends that don't require a `location` argument, such as [MemDOWN](https://github.com/rvagg/memdown).
+
+For example:
+
+```js
+var levelup = require('levelup')
+var memdown = require('memdown')
+var db = levelup({ db: memdown })
+```
+
+The `levelup(db, callback)` form (with optional `callback`) is only available where `db` is a factory function, as would be provided as a `'db'` property on an `options` object (see below). Only for back-ends that don't require a `location` argument, such as [MemDOWN](https://github.com/rvagg/memdown).
+
+For example:
+
+```js
+var levelup = require('levelup')
+var memdown = require('memdown')
+var db = levelup(memdown)
+```
+
+#### `options`
+
+`levelup()` takes an optional options object as its second argument; the following properties are accepted:
+
+* `'createIfMissing'` *(boolean, default: `true`)*: If `true`, will initialise an empty database at the specified location if one doesn't already exist. If `false` and a database doesn't exist you will receive an error in your `open()` callback and your database won't open.
+
+* `'errorIfExists'` *(boolean, default: `false`)*: If `true`, you will receive an error in your `open()` callback if the database exists at the specified location.
+
+* `'compression'` *(boolean, default: `true`)*: If `true`, all *compressible* data will be run through the Snappy compression algorithm before being stored. Snappy is very fast and shouldn't gain much speed by disabling so leave this on unless you have good reason to turn it off.
+
+* `'cacheSize'` *(number, default: `8 * 1024 * 1024`)*: The size (in bytes) of the in-memory [LRU](http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used) cache with frequently used uncompressed block contents. 
+
+* `'keyEncoding'` and `'valueEncoding'` *(string, default: `'utf8'`)*: The encoding of the keys and values passed through Node.js' `Buffer` implementation (see [Buffer#toString()](http://nodejs.org/docs/latest/api/buffer.html#buffer_buf_tostring_encoding_start_end)).
+  <p><code>'utf8'</code> is the default encoding for both keys and values so you can simply pass in strings and expect strings from your <code>get()</code> operations. You can also pass <code>Buffer</code> objects as keys and/or values and conversion will be performed.</p>
+  <p>Supported encodings are: hex, utf8, ascii, binary, base64, ucs2, utf16le.</p>
+  <p><code>'json'</code> encoding is also supported, see below.</p>
+
+* `'db'` *(object, default: LevelDOWN)*: LevelUP is backed by [LevelDOWN](https://github.com/rvagg/node-leveldown/) to provide an interface to LevelDB. You can completely replace the use of LevelDOWN by providing a "factory" function that will return a LevelDOWN API compatible object given a `location` argument. For further information, see [MemDOWN](https://github.com/rvagg/node-memdown/), a fully LevelDOWN API compatible replacement that uses a memory store rather than LevelDB. Also se [...]
+
+Additionally, each of the main interface methods accept an optional options object that can be used to override `'keyEncoding'` and `'valueEncoding'`.
+
+--------------------------------------------------------
+<a name="open"></a>
+### db.open([callback])
+<code>open()</code> opens the underlying LevelDB store. In general **you should never need to call this method directly** as it's automatically called by <a href="#ctor"><code>levelup()</code></a>.
+
+However, it is possible to *reopen* a database after it has been closed with <a href="#close"><code>close()</code></a>, although this is not generally advised.
+
+--------------------------------------------------------
+<a name="close"></a>
+### db.close([callback])
+<code>close()</code> closes the underlying LevelDB store. The callback will receive any error encountered during closing as the first argument.
+
+You should always clean up your LevelUP instance by calling `close()` when you no longer need it to free up resources. A LevelDB store cannot be opened by multiple instances of LevelDB/LevelUP simultaneously.
+
+--------------------------------------------------------
+<a name="put"></a>
+### db.put(key, value[, options][, callback])
+<code>put()</code> is the primary method for inserting data into the store. Both the `key` and `value` can be arbitrary data objects.
+
+The callback argument is optional but if you don't provide one and an error occurs then expect the error to be thrown.
+
+#### `options`
+
+Encoding of the `key` and `value` objects will adhere to `'keyEncoding'` and `'valueEncoding'` options provided to <a href="#ctor"><code>levelup()</code></a>, although you can provide alternative encoding settings in the options for `put()` (it's recommended that you stay consistent in your encoding of keys and values in a single store).
+
+If you provide a `'sync'` value of `true` in your `options` object, LevelDB will perform a synchronous write of the data; although the operation will be asynchronous as far as Node is concerned. Normally, LevelDB passes the data to the operating system for writing and returns immediately, however a synchronous write will use `fsync()` or equivalent so your callback won't be triggered until the data is actually on disk. Synchronous filesystem writes are **significantly** slower than async [...]
+
+--------------------------------------------------------
+<a name="get"></a>
+### db.get(key[, options][, callback])
+<code>get()</code> is the primary method for fetching data from the store. The `key` can be an arbitrary data object. If it doesn't exist in the store then the callback will receive an error as its first argument. A not-found err object will be of type `'NotFoundError'` so you can `err.type == 'NotFoundError'` or you can perform a truthy test on the property `err.notFound`.
+
+```js
+db.get('foo', function (err, value) {
+  if (err) {
+    if (err.notFound) {
+      // handle a 'NotFoundError' here
+      return
+    }
+    // I/O or other error, pass it up the callback chain
+    return callback(err)
+  }
+
+  // .. handle `value` here
+})
+```
+
+#### `options`
+
+Encoding of the `key` object will adhere to the `'keyEncoding'` option provided to <a href="#ctor"><code>levelup()</code></a>, although you can provide alternative encoding settings in the options for `get()` (it's recommended that you stay consistent in your encoding of keys and values in a single store).
+
+LevelDB will by default fill the in-memory LRU Cache with data from a call to get. Disabling this is done by setting `fillCache` to `false`. 
+
+--------------------------------------------------------
+<a name="del"></a>
+### db.del(key[, options][, callback])
+<code>del()</code> is the primary method for removing data from the store.
+
+#### `options`
+
+Encoding of the `key` object will adhere to the `'keyEncoding'` option provided to <a href="#ctor"><code>levelup()</code></a>, although you can provide alternative encoding settings in the options for `del()` (it's recommended that you stay consistent in your encoding of keys and values in a single store).
+
+A `'sync'` option can also be passed, see <a href="#put"><code>put()</code></a> for details on how this works.
+
+--------------------------------------------------------
+<a name="batch"></a>
+### db.batch(array[, options][, callback]) *(array form)*
+<code>batch()</code> can be used for very fast bulk-write operations (both *put* and *delete*). The `array` argument should contain a list of operations to be executed sequentially, although as a whole they are performed as an atomic operation inside LevelDB. Each operation is contained in an object having the following properties: `type`, `key`, `value`, where the *type* is either `'put'` or `'del'`. In the case of `'del'` the `'value'` property is ignored. Any entries with a `'key'` of [...]
+
+```js
+var ops = [
+    { type: 'del', key: 'father' }
+  , { type: 'put', key: 'name', value: 'Yuri Irsenovich Kim' }
+  , { type: 'put', key: 'dob', value: '16 February 1941' }
+  , { type: 'put', key: 'spouse', value: 'Kim Young-sook' }
+  , { type: 'put', key: 'occupation', value: 'Clown' }
+]
+
+db.batch(ops, function (err) {
+  if (err) return console.log('Ooops!', err)
+  console.log('Great success dear leader!')
+})
+```
+
+#### `options`
+
+See <a href="#put"><code>put()</code></a> for a discussion on the `options` object. You can overwrite default `'keyEncoding'` and `'valueEncoding'` and also specify the use of `sync` filesystem operations.
+
+In addition to encoding options for the whole batch you can also overwrite the encoding per operation, like:
+
+```js
+var ops = [{
+    type          : 'put'
+  , key           : new Buffer([1, 2, 3])
+  , value         : { some: 'json' }
+  , keyEncoding   : 'binary'
+  , valueEncoding : 'json'
+}]
+```
+
+--------------------------------------------------------
+<a name="batch_chained"></a>
+### db.batch() *(chained form)*
+<code>batch()</code>, when called with no arguments will return a `Batch` object which can be used to build, and eventually commit, an atomic LevelDB batch operation. Depending on how it's used, it is possible to obtain greater performance when using the chained form of `batch()` over the array form.
+
+```js
+db.batch()
+  .del('father')
+  .put('name', 'Yuri Irsenovich Kim')
+  .put('dob', '16 February 1941')
+  .put('spouse', 'Kim Young-sook')
+  .put('occupation', 'Clown')
+  .write(function () { console.log('Done!') })
+```
+
+<b><code>batch.put(key, value[, options])</code></b>
+
+Queue a *put* operation on the current batch, not committed until a `write()` is called on the batch.
+
+The optional `options` argument can be used to override the default `'keyEncoding'` and/or `'valueEncoding'`.
+
+This method may `throw` a `WriteError` if there is a problem with your put (such as the `value` being `null` or `undefined`).
+
+<b><code>batch.del(key[, options])</code></b>
+
+Queue a *del* operation on the current batch, not committed until a `write()` is called on the batch.
+
+The optional `options` argument can be used to override the default `'keyEncoding'`.
+
+This method may `throw` a `WriteError` if there is a problem with your delete.
+
+<b><code>batch.clear()</code></b>
+
+Clear all queued operations on the current batch, any previous operations will be discarded.
+
+<b><code>batch.write([callback])</code></b>
+
+Commit the queued operations for this batch. All operations not *cleared* will be written to the database atomically, that is, they will either all succeed or fail with no partial commits. The optional `callback` will be called when the operation has completed with an *error* argument if an error has occurred; if no `callback` is supplied and an error occurs then this method will `throw` a `WriteError`.
+
+
+--------------------------------------------------------
+<a name="isOpen"></a>
+### db.isOpen()
+
+A LevelUP object can be in one of the following states:
+
+  * *"new"*     - newly created, not opened or closed
+  * *"opening"* - waiting for the database to be opened
+  * *"open"*    - successfully opened the database, available for use
+  * *"closing"* - waiting for the database to be closed
+  * *"closed"*  - database has been successfully closed, should not be used
+
+`isOpen()` will return `true` only when the state is "open".
+
+--------------------------------------------------------
+<a name="isClosed"></a>
+### db.isClosed()
+
+*See <a href="#put"><code>isOpen()</code></a>*
+
+`isClosed()` will return `true` only when the state is "closing" *or* "closed", it can be useful for determining if read and write operations are permissible.
+
+--------------------------------------------------------
+<a name="createReadStream"></a>
+### db.createReadStream([options])
+
+You can obtain a **ReadStream** of the full database by calling the `createReadStream()` method. The resulting stream is a complete Node.js-style [Readable Stream](http://nodejs.org/docs/latest/api/stream.html#stream_readable_stream) where `'data'` events emit objects with `'key'` and `'value'` pairs. You can also use the `start`, `end` and `limit` options to control the range of keys that are streamed.
+
+```js
+db.createReadStream()
+  .on('data', function (data) {
+    console.log(data.key, '=', data.value)
+  })
+  .on('error', function (err) {
+    console.log('Oh my!', err)
+  })
+  .on('close', function () {
+    console.log('Stream closed')
+  })
+  .on('end', function () {
+    console.log('Stream closed')
+  })
+```
+
+The standard `pause()`, `resume()` and `destroy()` methods are implemented on the ReadStream, as is `pipe()` (see below). `'data'`, '`error'`, `'end'` and `'close'` events are emitted.
+
+Additionally, you can supply an options object as the first parameter to `createReadStream()` with the following options:
+
+* `'start'`: the key you wish to start the read at. By default it will start at the beginning of the store. Note that the *start* doesn't have to be an actual key that exists, LevelDB will simply find the *next* key, greater than the key you provide.
+
+* `'end'`: the key you wish to end the read on. By default it will continue until the end of the store. Again, the *end* doesn't have to be an actual key as an (inclusive) `<=`-type operation is performed to detect the end. You can also use the `destroy()` method instead of supplying an `'end'` parameter to achieve the same effect.
+
+* `'reverse'` *(boolean, default: `false`)*: a boolean, set to true if you want the stream to go in reverse order. Beware that due to the way LevelDB works, a reverse seek will be slower than a forward seek.
+
+* `'keys'` *(boolean, default: `true`)*: whether the `'data'` event should contain keys. If set to `true` and `'values'` set to `false` then `'data'` events will simply be keys, rather than objects with a `'key'` property. Used internally by the `createKeyStream()` method.
+
+* `'values'` *(boolean, default: `true`)*: whether the `'data'` event should contain values. If set to `true` and `'keys'` set to `false` then `'data'` events will simply be values, rather than objects with a `'value'` property. Used internally by the `createValueStream()` method.
+
+* `'limit'` *(number, default: `-1`)*: limit the number of results collected by this stream. This number represents a *maximum* number of results and may not be reached if you get to the end of the store or your `'end'` value first. A value of `-1` means there is no limit.
+
+* `'fillCache'` *(boolean, default: `false`)*: wheather LevelDB's LRU-cache should be filled with data read.
+
+* `'keyEncoding'` / `'valueEncoding'` *(string)*: the encoding applied to each read piece of data.
+
+--------------------------------------------------------
+<a name="createKeyStream"></a>
+### db.createKeyStream([options])
+
+A **KeyStream** is a **ReadStream** where the `'data'` events are simply the keys from the database so it can be used like a traditional stream rather than an object stream.
+
+You can obtain a KeyStream either by calling the `createKeyStream()` method on a LevelUP object or by passing passing an options object to `createReadStream()` with `keys` set to `true` and `values` set to `false`.
+
+```js
+db.createKeyStream()
+  .on('data', function (data) {
+    console.log('key=', data)
+  })
+
+// same as:
+db.createReadStream({ keys: true, values: false })
+  .on('data', function (data) {
+    console.log('key=', data)
+  })
+```
+
+--------------------------------------------------------
+<a name="createValueStream"></a>
+### db.createValueStream([options])
+
+A **ValueStream** is a **ReadStream** where the `'data'` events are simply the values from the database so it can be used like a traditional stream rather than an object stream.
+
+You can obtain a ValueStream either by calling the `createValueStream()` method on a LevelUP object or by passing passing an options object to `createReadStream()` with `values` set to `true` and `keys` set to `false`.
+
+```js
+db.createValueStream()
+  .on('data', function (data) {
+    console.log('value=', data)
+  })
+
+// same as:
+db.createReadStream({ keys: false, values: true })
+  .on('data', function (data) {
+    console.log('value=', data)
+  })
+```
+
+--------------------------------------------------------
+<a name="createWriteStream"></a>
+### db.createWriteStream([options])
+
+A **WriteStream** can be obtained by calling the `createWriteStream()` method. The resulting stream is a complete Node.js-style [Writable Stream](http://nodejs.org/docs/latest/api/stream.html#stream_writable_stream) which accepts objects with `'key'` and `'value'` pairs on its `write()` method.
+
+The WriteStream will buffer writes and submit them as a `batch()` operations where writes occur *within the same tick*.
+
+```js
+var ws = db.createWriteStream()
+
+ws.on('error', function (err) {
+  console.log('Oh my!', err)
+})
+ws.on('close', function () {
+  console.log('Stream closed')
+})
+
+ws.write({ key: 'name', value: 'Yuri Irsenovich Kim' })
+ws.write({ key: 'dob', value: '16 February 1941' })
+ws.write({ key: 'spouse', value: 'Kim Young-sook' })
+ws.write({ key: 'occupation', value: 'Clown' })
+ws.end()
+```
+
+The standard `write()`, `end()`, `destroy()` and `destroySoon()` methods are implemented on the WriteStream. `'drain'`, `'error'`, `'close'` and `'pipe'` events are emitted.
+
+You can specify encodings both for the whole stream and individual entries:
+
+To set the encoding for the whole stream, provide an options object as the first parameter to `createWriteStream()` with `'keyEncoding'` and/or `'valueEncoding'`.
+
+To set the encoding for an individual entry:
+
+```js
+writeStream.write({
+    key           : new Buffer([1, 2, 3])
+  , value         : { some: 'json' }
+  , keyEncoding   : 'binary'
+  , valueEncoding : 'json'
+})
+```
+
+#### write({ type: 'put' })
+
+If individual `write()` operations are performed with a `'type'` property of `'del'`, they will be passed on as `'del'` operations to the batch.
+
+```js
+var ws = db.createWriteStream()
+
+ws.on('error', function (err) {
+  console.log('Oh my!', err)
+})
+ws.on('close', function () {
+  console.log('Stream closed')
+})
+
+ws.write({ type: 'del', key: 'name' })
+ws.write({ type: 'del', key: 'dob' })
+ws.write({ type: 'put', key: 'spouse' })
+ws.write({ type: 'del', key: 'occupation' })
+ws.end()
+```
+
+#### db.createWriteStream({ type: 'del' })
+
+If the *WriteStream* is created with a `'type'` option of `'del'`, all `write()` operations will be interpreted as `'del'`, unless explicitly specified as `'put'`.
+
+```js
+var ws = db.createWriteStream({ type: 'del' })
+
+ws.on('error', function (err) {
+  console.log('Oh my!', err)
+})
+ws.on('close', function () {
+  console.log('Stream closed')
+})
+
+ws.write({ key: 'name' })
+ws.write({ key: 'dob' })
+// but it can be overridden
+ws.write({ type: 'put', key: 'spouse', value: 'Ri Sol-ju' })
+ws.write({ key: 'occupation' })
+ws.end()
+```
+
+#### Pipes and Node Stream compatibility
+
+A ReadStream can be piped directly to a WriteStream, allowing for easy copying of an entire database. A simple `copy()` operation is included in LevelUP that performs exactly this on two open databases:
+
+```js
+function copy (srcdb, dstdb, callback) {
+  srcdb.createReadStream().pipe(dstdb.createWriteStream()).on('close', callback)
+}
+```
+
+The ReadStream is also [fstream](https://github.com/isaacs/fstream)-compatible which means you should be able to pipe to and from fstreams. So you can serialize and deserialize an entire database to a directory where keys are filenames and values are their contents, or even into a *tar* file using [node-tar](https://github.com/isaacs/node-tar). See the [fstream functional test](https://github.com/rvagg/node-levelup/blob/master/test/functional/fstream-test.js) for an example. *(Note: I'm  [...]
+
+KeyStreams and ValueStreams can be treated like standard streams of raw data. If `'keyEncoding'` or `'valueEncoding'` is set to `'binary'` the `'data'` events will simply be standard Node `Buffer` objects straight out of the data store.
+
+
+--------------------------------------------------------
+<a name='approximateSize'></a>
+### db.db.approximateSize(start, end, callback)
+<code>approximateSize()</code> can used to get the approximate number of bytes of file system space used by the range `[start..end)`. The result may not include recently written data.
+
+```js
+var db = require('level')('./huge.db')
+
+db.db.approximateSize('a', 'c', function (err, size) {
+  if (err) return console.error('Ooops!', err)
+  console.log('Approximate size of range is %d', size)
+})
+```
+
+**Note:** `approximateSize()` is available via [LevelDOWN](https://github.com/rvagg/node-leveldown/), which by default is accessible as the `db` property of your LevelUP instance. This is a specific LevelDB operation and is not likely to be available where you replace LevelDOWN with an alternative back-end via the `'db'` option.
+
+
+--------------------------------------------------------
+<a name='getProperty'></a>
+### db.db.getProperty(property)
+<code>getProperty</code> can be used to get internal details from LevelDB. When issued with a valid property string, a readable string will be returned (this method is synchronous).
+
+Currently, the only valid properties are:
+
+* <b><code>'leveldb.num-files-at-levelN'</code></b>: returns the number of files at level *N*, where N is an integer representing a valid level (e.g. "0").
+
+* <b><code>'leveldb.stats'</code></b>: returns a multi-line string describing statistics about LevelDB's internal operation.
+
+* <b><code>'leveldb.sstables'</code></b>: returns a multi-line string describing all of the *sstables* that make up contents of the current database.
+
+
+```js
+var db = require('level')('./huge.db')
+console.log(db.db.getProperty('leveldb.num-files-at-level3'))
+// → '243'
+```
+
+**Note:** `getProperty()` is available via [LevelDOWN](https://github.com/rvagg/node-leveldown/), which by default is accessible as the `db` property of your LevelUP instance. This is a specific LevelDB operation and is not likely to be available where you replace LevelDOWN with an alternative back-end via the `'db'` option.
+
+
+--------------------------------------------------------
+<a name="destroy"></a>
+### leveldown.destroy(location, callback)
+<code>destroy()</code> is used to completely remove an existing LevelDB database directory. You can use this function in place of a full directory *rm* if you want to be sure to only remove LevelDB-related files. If the directory only contains LevelDB files, the directory itself will be removed as well. If there are additional, non-LevelDB files in the directory, those files, and the directory, will be left alone.
+
+The callback will be called when the destroy operation is complete, with a possible `error` argument.
+
+**Note:** `destroy()` is available via [LevelDOWN](https://github.com/rvagg/node-leveldown/) which you will have to install seperately, e.g.:
+
+```js
+require('leveldown').destroy('./huge.db', function (err) { console.log('done!') })
+```
+
+--------------------------------------------------------
+<a name="repair"></a>
+### leveldown.repair(location, callback)
+<code>repair()</code> can be used to attempt a restoration of a damaged LevelDB store. From the LevelDB documentation:
+
+> If a DB cannot be opened, you may attempt to call this method to resurrect as much of the contents of the database as possible. Some data may be lost, so be careful when calling this function on a database that contains important information.
+
+You will find information on the *repair* operation in the *LOG* file inside the store directory. 
+
+A `repair()` can also be used to perform a compaction of the LevelDB log into table files.
+
+The callback will be called when the repair operation is complete, with a possible `error` argument.
+
+**Note:** `repair()` is available via [LevelDOWN](https://github.com/rvagg/node-leveldown/) which you will have to install seperately, e.g.:
+
+```js
+require('leveldown').repair('./huge.db', function (err) { console.log('done!') })
+```
+
+--------------------------------------------------------
+
+<a name="events"></a>
+Events
+------
+
+LevelUP emits events when the callbacks to the corresponding methods are called.
+
+* `db.emit('put', key, value)` emitted when a new value is `'put'`
+* `db.emit('del', key)` emitted when a value is deleted
+* `db.emit('batch', ary)` emitted when a batch operation has executed
+* `db.emit('ready')` emitted when the database has opened (`'open'` is synonym)
+* `db.emit('closed')` emitted when the database has closed
+* `db.emit('opening')` emitted when the database is opening
+* `db.emit('closing')` emitted when the database is closing
+
+If you do not pass a callback to an async function, and there is an error, LevelUP will `emit('error', err)` instead.
+
+<a name="json"></a>
+JSON data
+---------
+
+You specify `'json'` encoding for both keys and/or values, you can then supply JavaScript objects to LevelUP and receive them from all fetch operations, including ReadStreams. LevelUP will automatically *stringify* your objects and store them as *utf8* and parse the strings back into objects before passing them back to you.
+
+<a name="custom_encodings"></a>
+Custom encodings
+----------------
+
+A custom encoding may be provided by passing in an object as an value for `keyEncoding` or `valueEncoding` (wherever accepted), it must have the following properties:
+
+```js
+{
+    encode : function (val) { ... }
+  , decode : function (val) { ... }
+  , buffer : boolean // encode returns a buffer-like and decode accepts a buffer
+  , type   : String  // name of this encoding type.
+}
+```
+
+*"buffer-like"* means either a `Buffer` if running in Node, or a Uint8Array if in a browser. Use [bops](https://github.com/chrisdickinson/bops) to get portable binary operations.
+
+<a name="extending"></a>
+Extending LevelUP
+-----------------
+
+A list of <a href="https://github.com/rvagg/node-levelup/wiki/Modules"><b>Node.js LevelDB modules and projects</b></a> can be found in the wiki.
+
+When attempting to extend the functionality of LevelUP, it is recommended that you consider using [level-hooks](https://github.com/dominictarr/level-hooks) and/or [level-sublevel](https://github.com/dominictarr/level-sublevel). **level-sublevel** is particularly helpful for keeping additional, extension-specific, data in a LevelDB store. It allows you to partition a LevelUP instance into multiple sub-instances that each correspond to discrete namespaced key ranges.
+
+<a name="multiproc"></a>
+Multi-process access
+--------------------
+
+LevelDB is thread-safe but is **not** suitable for accessing with multiple processes. You should only ever have a LevelDB database open from a single Node.js process. Node.js clusters are made up of multiple processes so a LevelUP instance cannot be shared between them either.
+
+See the <a href="https://github.com/rvagg/node-levelup/wiki/Modules"><b>wiki</b></a> for some LevelUP extensions, including [multilevel](https://github.com/juliangruber/multilevel), that may help if you require a single data store to be shared across processes.
+
+<a name="support"></a>
+Getting support
+---------------
+
+There are multiple ways you can find help in using LevelDB in Node.js:
+
+ * **IRC:** you'll find an active group of LevelUP users in the **##leveldb** channel on Freenode, including most of the contributors to this project.
+ * **Mailing list:** there is an active [Node.js LevelDB](https://groups.google.com/forum/#!forum/node-levelup) Google Group.
+ * **GitHub:** you're welcome to open an issue here on this GitHub repository if you have a question.
+
+<a name="contributing"></a>
+Contributing
+------------
+
+LevelUP is an **OPEN Open Source Project**. This means that:
+
+> Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project.
+
+See the [CONTRIBUTING.md](https://github.com/rvagg/node-levelup/blob/master/CONTRIBUTING.md) file for more details.
+
+### Contributors
+
+LevelUP is only possible due to the excellent work of the following contributors:
+
+<table><tbody>
+<tr><th align="left">Rod Vagg</th><td><a href="https://github.com/rvagg">GitHub/rvagg</a></td><td><a href="http://twitter.com/rvagg">Twitter/@rvagg</a></td></tr>
+<tr><th align="left">John Chesley</th><td><a href="https://github.com/chesles/">GitHub/chesles</a></td><td><a href="http://twitter.com/chesles">Twitter/@chesles</a></td></tr>
+<tr><th align="left">Jake Verbaten</th><td><a href="https://github.com/raynos">GitHub/raynos</a></td><td><a href="http://twitter.com/raynos2">Twitter/@raynos2</a></td></tr>
+<tr><th align="left">Dominic Tarr</th><td><a href="https://github.com/dominictarr">GitHub/dominictarr</a></td><td><a href="http://twitter.com/dominictarr">Twitter/@dominictarr</a></td></tr>
+<tr><th align="left">Max Ogden</th><td><a href="https://github.com/maxogden">GitHub/maxogden</a></td><td><a href="http://twitter.com/maxogden">Twitter/@maxogden</a></td></tr>
+<tr><th align="left">Lars-Magnus Skog</th><td><a href="https://github.com/ralphtheninja">GitHub/ralphtheninja</a></td><td><a href="http://twitter.com/ralphtheninja">Twitter/@ralphtheninja</a></td></tr>
+<tr><th align="left">David Björklund</th><td><a href="https://github.com/kesla">GitHub/kesla</a></td><td><a href="http://twitter.com/david_bjorklund">Twitter/@david_bjorklund</a></td></tr>
+<tr><th align="left">Julian Gruber</th><td><a href="https://github.com/juliangruber">GitHub/juliangruber</a></td><td><a href="http://twitter.com/juliangruber">Twitter/@juliangruber</a></td></tr>
+<tr><th align="left">Paolo Fragomeni</th><td><a href="https://github.com/hij1nx">GitHub/hij1nx</a></td><td><a href="http://twitter.com/hij1nx">Twitter/@hij1nx</a></td></tr>
+<tr><th align="left">Anton Whalley</th><td><a href="https://github.com/No9">GitHub/No9</a></td><td><a href="https://twitter.com/antonwhalley">Twitter/@antonwhalley</a></td></tr>
+<tr><th align="left">Matteo Collina</th><td><a href="https://github.com/mcollina">GitHub/mcollina</a></td><td><a href="https://twitter.com/matteocollina">Twitter/@matteocollina</a></td></tr>
+<tr><th align="left">Pedro Teixeira</th><td><a href="https://github.com/pgte">GitHub/pgte</a></td><td><a href="https://twitter.com/pgte">Twitter/@pgte</a></td></tr>
+<tr><th align="left">James Halliday</th><td><a href="https://github.com/substack">GitHub/substack</a></td><td><a href="https://twitter.com/substack">Twitter/@substack</a></td></tr>
+</tbody></table>
+
+### Windows
+
+A large portion of the Windows support comes from code by [Krzysztof Kowalczyk](http://blog.kowalczyk.info/) [@kjk](https://twitter.com/kjk), see his Windows LevelDB port [here](http://code.google.com/r/kkowalczyk-leveldb/). If you're using LevelUP on Windows, you should give him your thanks!
+
+
+<a name="licence"></a>
+Licence & copyright
+-------------------
+
+Copyright (c) 2012-2013 LevelUP contributors (listed above).
+
+LevelUP is licensed under an MIT +no-false-attribs license. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE file for more details.
+
+=======
+*LevelUP builds on the excellent work of the LevelDB and Snappy teams from Google and additional contributors. LevelDB and Snappy are both issued under the [New BSD Licence](http://opensource.org/licenses/BSD-3-Clause).*
diff --git a/buster.js b/buster.js
new file mode 100644
index 0000000..fdc0a67
--- /dev/null
+++ b/buster.js
@@ -0,0 +1,7 @@
+var config = module.exports
+
+config['unit'] = {
+    environment: 'node'
+  , tests: [ 'test/*-test.js' ]
+  , libs: [ 'test/common.js' ]
+}
\ No newline at end of file
diff --git a/lib/batch.js b/lib/batch.js
new file mode 100644
index 0000000..b63bd44
--- /dev/null
+++ b/lib/batch.js
@@ -0,0 +1,78 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License
+ * <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var util          = require('./util')
+  , WriteError    = require('./errors').WriteError
+
+  , getOptions    = util.getOptions
+  , dispatchError = util.dispatchError
+
+function Batch (levelup) {
+  this._levelup = levelup
+  this.batch = levelup.db.batch()
+  this.ops = []
+}
+
+Batch.prototype.put = function (key_, value_, options) {
+  options = getOptions(this._levelup, options)
+
+  var key   = util.encodeKey(key_, options)
+    , value = util.encodeValue(value_, options)
+
+  try {
+    this.batch.put(key, value)
+  } catch (e) {
+    throw new WriteError(e)
+  }
+  this.ops.push({ type : 'put', key : key, value : value })
+
+  return this
+}
+
+Batch.prototype.del = function (key_, options) {
+  options = getOptions(this._levelup, options)
+
+  var key = util.encodeKey(key_, options)
+
+  try {
+    this.batch.del(key)
+  } catch (err) {
+    throw new WriteError(err)
+  }
+  this.ops.push({ type : 'del', key : key })
+
+  return this
+}
+
+Batch.prototype.clear = function () {
+  try {
+    this.batch.clear()
+  } catch (err) {
+    throw new WriteError(err)
+  }
+
+  this.ops = []
+  return this
+}
+
+Batch.prototype.write = function (callback) {
+  var levelup = this._levelup
+    , ops     = this.ops
+
+  try {
+    this.batch.write(function (err) {
+      if (err)
+        return dispatchError(levelup, new WriteError(err), callback)
+      levelup.emit('batch', ops)
+      if (callback)
+        callback()
+    })
+  } catch (err) {
+    throw new WriteError(err)
+  }
+}
+
+module.exports = Batch
diff --git a/lib/errors.js b/lib/errors.js
new file mode 100644
index 0000000..6114ccd
--- /dev/null
+++ b/lib/errors.js
@@ -0,0 +1,22 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License
+ * <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var createError   = require('errno').create
+  , LevelUPError  = createError('LevelUPError')
+  , NotFoundError = createError('NotFoundError', LevelUPError)
+
+NotFoundError.prototype.notFound = true
+NotFoundError.prototype.status   = 404
+
+module.exports = {
+    LevelUPError        : LevelUPError
+  , InitializationError : createError('InitializationError', LevelUPError)
+  , OpenError           : createError('OpenError', LevelUPError)
+  , ReadError           : createError('ReadError', LevelUPError)
+  , WriteError          : createError('WriteError', LevelUPError)
+  , NotFoundError       : NotFoundError
+  , EncodingError       : createError('EncodingError', LevelUPError)
+}
\ No newline at end of file
diff --git a/lib/levelup.js b/lib/levelup.js
new file mode 100644
index 0000000..22b6721
--- /dev/null
+++ b/lib/levelup.js
@@ -0,0 +1,435 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License
+ * <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var EventEmitter   = require('events').EventEmitter
+  , inherits       = require('util').inherits
+  , extend         = require('xtend')
+  , prr            = require('prr')
+  , DeferredLevelDOWN = require('deferred-leveldown')
+
+  , WriteError     = require('./errors').WriteError
+  , ReadError      = require('./errors').ReadError
+  , NotFoundError  = require('./errors').NotFoundError
+  , OpenError      = require('./errors').OpenError
+  , EncodingError  = require('./errors').EncodingError
+  , InitializationError = require('./errors').InitializationError
+
+  , ReadStream     = require('./read-stream')
+  , WriteStream    = require('./write-stream')
+  , util           = require('./util')
+  , Batch          = require('./batch')
+
+  , getOptions     = util.getOptions
+  , defaultOptions = util.defaultOptions
+  , getLevelDOWN   = util.getLevelDOWN
+  , dispatchError  = util.dispatchError
+
+function getCallback (options, callback) {
+  return typeof options == 'function' ? options : callback
+}
+
+// Possible LevelUP#_status values:
+//  - 'new'     - newly created, not opened or closed
+//  - 'opening' - waiting for the database to be opened, post open()
+//  - 'open'    - successfully opened the database, available for use
+//  - 'closing' - waiting for the database to be closed, post close()
+//  - 'closed'  - database has been successfully closed, should not be
+//                 used except for another open() operation
+
+function LevelUP (location, options, callback) {
+  if (!(this instanceof LevelUP))
+    return new LevelUP(location, options, callback)
+
+  var error
+
+  EventEmitter.call(this)
+  this.setMaxListeners(Infinity)
+
+  if (typeof location == 'function') {
+    options = typeof options == 'object' ? options : {}
+    options.db = location
+    location = null
+  } else if (typeof location == 'object' && typeof location.db == 'function') {
+    options = location
+    location = null
+  }
+
+  if (typeof options == 'function') {
+    callback = options
+    options  = {}
+  }
+
+  if ((!options || typeof options.db != 'function') && typeof location != 'string') {
+    error = new InitializationError(
+        'Must provide a location for the database')
+    if (callback) {
+      return process.nextTick(function () {
+        callback(error)
+      })
+    }
+    throw error
+  }
+
+  options      = getOptions(this, options)
+  this.options = extend(defaultOptions, options)
+  this._status = 'new'
+  // set this.location as enumerable but not configurable or writable
+  prr(this, 'location', location, 'e')
+
+  this.open(callback)
+}
+
+inherits(LevelUP, EventEmitter)
+
+LevelUP.prototype.open = function (callback) {
+  var self = this
+    , dbFactory
+    , db
+
+  if (this.isOpen()) {
+    if (callback)
+      process.nextTick(function () { callback(null, self) })
+    return this
+  }
+
+  if (this._isOpening()) {
+    return callback && this.once(
+        'open'
+      , function () { callback(null, self) }
+    )
+  }
+
+  this.emit('opening')
+
+  this._status = 'opening'
+  this.db      = new DeferredLevelDOWN(this.location)
+  dbFactory    = this.options.db || getLevelDOWN()
+  db           = dbFactory(this.location)
+
+  db.open(this.options, function (err) {
+    if (err) {
+      return dispatchError(self, new OpenError(err), callback)
+    } else {
+      self.db.setDb(db)
+      self.db = db
+      self._status = 'open'
+      if (callback)
+        callback(null, self)
+      self.emit('open')
+      self.emit('ready')
+    }
+  })
+}
+
+LevelUP.prototype.close = function (callback) {
+  var self = this
+
+  if (this.isOpen()) {
+    this._status = 'closing'
+    this.db.close(function () {
+      self._status = 'closed'
+      self.emit('closed')
+      if (callback)
+        callback.apply(null, arguments)
+    })
+    this.emit('closing')
+    this.db = null
+  } else if (this._status == 'closed' && callback) {
+    return process.nextTick(callback)
+  } else if (this._status == 'closing' && callback) {
+    this.once('closed', callback)
+  } else if (this._isOpening()) {
+    this.once('open', function () {
+      self.close(callback)
+    })
+  }
+}
+
+LevelUP.prototype.isOpen = function () {
+  return this._status == 'open'
+}
+
+LevelUP.prototype._isOpening = function () {
+  return this._status == 'opening'
+}
+
+LevelUP.prototype.isClosed = function () {
+  return (/^clos/).test(this._status)
+}
+
+LevelUP.prototype.get = function (key_, options, callback) {
+  var self = this
+    , key
+
+  callback = getCallback(options, callback)
+
+  if (typeof callback != 'function') {
+    return dispatchError(
+        this
+      , new ReadError('get() requires key and callback arguments')
+    )
+  }
+
+  if (!this._isOpening() && !this.isOpen()) {
+    return dispatchError(
+        this
+      , new ReadError('Database is not open')
+      , callback
+    )
+  }
+
+  options = util.getOptions(this, options)
+  key = util.encodeKey(key_, options)
+
+  options.asBuffer = util.isValueAsBuffer(options)
+
+  this.db.get(key, options, function (err, value) {
+    if (err) {
+      if ((/notfound/i).test(err)) {
+        err = new NotFoundError(
+            'Key not found in database [' + key_ + ']', err)
+      } else {
+        err = new ReadError(err)
+      }
+      return dispatchError(self, err, callback)
+    }
+    if (callback) {
+      try {
+        value = util.decodeValue(value, options)
+      } catch (e) {
+        return callback(new EncodingError(e))
+      }
+      callback(null, value)
+    }
+  })
+}
+
+LevelUP.prototype.put = function (key_, value_, options, callback) {
+  var self = this
+    , key
+    , value
+
+  callback = getCallback(options, callback)
+
+  if (key_ === null || key_ === undefined
+        || value_ === null || value_ === undefined) {
+    return dispatchError(
+        this
+       , new WriteError('put() requires key and value arguments')
+       , callback
+    )
+  }
+
+  if (!this._isOpening() && !this.isOpen()) {
+    return dispatchError(
+        this
+      , new WriteError('Database is not open')
+      , callback
+    )
+  }
+
+  options = getOptions(this, options)
+  key     = util.encodeKey(key_, options)
+  value   = util.encodeValue(value_, options)
+
+  this.db.put(key, value, options, function (err) {
+    if (err) {
+      return dispatchError(self, new WriteError(err), callback)
+    } else {
+      self.emit('put', key_, value_)
+      if (callback)
+        callback()
+    }
+  })
+}
+
+LevelUP.prototype.del = function (key_, options, callback) {
+  var self = this
+    , key
+
+  callback = getCallback(options, callback)
+
+  if (key_ === null || key_ === undefined) {
+    return dispatchError(
+        this
+      , new WriteError('del() requires a key argument')
+      , callback
+    )
+  }
+
+  if (!this._isOpening() && !this.isOpen()) {
+    return dispatchError(
+        this
+      , new WriteError('Database is not open')
+      , callback
+    )
+  }
+
+  options = getOptions(this, options)
+  key     = util.encodeKey(key_, options)
+
+  this.db.del(key, options, function (err) {
+    if (err) {
+      return dispatchError(self, new WriteError(err), callback)
+    } else {
+      self.emit('del', key_)
+      if (callback)
+        callback()
+    }
+  })
+}
+
+LevelUP.prototype.batch = function (arr_, options, callback) {
+  var self = this
+    , keyEnc
+    , valueEnc
+    , arr
+
+  if (!arguments.length)
+    return new Batch(this)
+
+  callback = getCallback(options, callback)
+
+  if (!Array.isArray(arr_)) {
+    return dispatchError(
+        this
+      , new WriteError('batch() requires an array argument')
+      , callback
+    )
+  }
+
+  if (!this._isOpening() && !this.isOpen()) {
+    return dispatchError(
+        this
+      , new WriteError('Database is not open')
+      , callback
+    )
+  }
+
+  options  = getOptions(this, options)
+  keyEnc   = options.keyEncoding
+  valueEnc = options.valueEncoding
+
+  arr = arr_.map(function (e) {
+    if (e.type === undefined || e.key === undefined)
+      return {}
+
+    // inherit encoding
+    var kEnc = e.keyEncoding || keyEnc
+      , vEnc = e.valueEncoding || e.encoding || valueEnc
+      , o
+
+    // If we're not dealing with plain utf8 strings or plain
+    // Buffers then we have to do some work on the array to
+    // encode the keys and/or values. This includes JSON types.
+
+    if (kEnc != 'utf8' && kEnc != 'binary'
+        || vEnc != 'utf8' && vEnc != 'binary') {
+      o = {
+          type: e.type
+        , key: util.encodeKey(e.key, options, e)
+      }
+
+      if (e.value !== undefined)
+        o.value = util.encodeValue(e.value, options, e)
+
+      return o
+    } else {
+      return e
+    }
+  })
+
+  this.db.batch(arr, options, function (err) {
+    if (err) {
+      return dispatchError(self, new WriteError(err), callback)
+    } else {
+      self.emit('batch', arr_)
+      if (callback)
+        callback()
+    }
+  })
+}
+
+// DEPRECATED: prefer accessing LevelDOWN for this: db.db.approximateSize()
+LevelUP.prototype.approximateSize = function (start_, end_, callback) {
+  var self = this
+    , start
+    , end
+
+  if (start_ === null || start_ === undefined
+        || end_ === null || end_ === undefined
+        || typeof callback != 'function') {
+    return dispatchError(
+        this
+      , new ReadError('approximateSize() requires start, end and callback arguments')
+      , callback
+    )
+  }
+
+  start = util.encodeKey(start_, this.options)
+  end   = util.encodeKey(end_, this.options)
+
+  if (!this._isOpening() && !this.isOpen()) {
+    return dispatchError(
+        this
+      , new WriteError('Database is not open')
+      , callback
+    )
+  }
+
+  this.db.approximateSize(start, end, function (err, size) {
+    if (err) {
+      return dispatchError(self, new OpenError(err), callback)
+    } else if (callback) {
+      callback(null, size)
+    }
+  })
+}
+
+LevelUP.prototype.readStream =
+LevelUP.prototype.createReadStream = function (options) {
+  var self = this
+  options = extend(this.options, options)
+  return new ReadStream(
+      options
+    , this
+    , function (options) {
+        return self.db.iterator(options)
+      }
+  )
+}
+
+LevelUP.prototype.keyStream =
+LevelUP.prototype.createKeyStream = function (options) {
+  return this.createReadStream(extend(options, { keys: true, values: false }))
+}
+
+LevelUP.prototype.valueStream =
+LevelUP.prototype.createValueStream = function (options) {
+  return this.createReadStream(extend(options, { keys: false, values: true }))
+}
+
+LevelUP.prototype.writeStream =
+LevelUP.prototype.createWriteStream = function (options) {
+  return new WriteStream(extend(options), this)
+}
+
+LevelUP.prototype.toString = function () {
+  return 'LevelUP'
+}
+
+function utilStatic (name) {
+  return function (location, callback) {
+    getLevelDOWN()[name](location, callback || function () {})
+  }
+}
+
+module.exports         = LevelUP
+module.exports.copy    = util.copy
+// DEPRECATED: prefer accessing LevelDOWN for this: require('leveldown').destroy()
+module.exports.destroy = utilStatic('destroy')
+// DEPRECATED: prefer accessing LevelDOWN for this: require('leveldown').repair()
+module.exports.repair  = utilStatic('repair')
diff --git a/lib/read-stream.js b/lib/read-stream.js
new file mode 100644
index 0000000..003da5d
--- /dev/null
+++ b/lib/read-stream.js
@@ -0,0 +1,126 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+// NOTE: we are fixed to readable-stream at 1.0.x for now
+// for pure Streams2 across Node versions
+var Readable      = require('readable-stream').Readable
+  , inherits      = require('util').inherits
+  , extend        = require('xtend')
+  , EncodingError = require('./errors').EncodingError
+  , util          = require('./util')
+
+  , defaultOptions = { keys: true, values: true }
+
+  , makeKeyValueData = function (key, value) {
+      return {
+          key: util.decodeKey(key, this._options)
+        , value: util.decodeValue(value, this._options)
+      }
+    }
+  , makeKeyData = function (key) {
+      return util.decodeKey(key, this._options)
+    }
+  , makeValueData = function (_, value) {
+      return util.decodeValue(value, this._options)
+    }
+  , makeNoData = function () { return null }
+
+function ReadStream (options, db, iteratorFactory) {
+  if (!(this instanceof ReadStream))
+    return new ReadStream(options, db, iteratorFactory)
+
+  Readable.call(this, { objectMode: true, highWaterMark: options.highWaterMark })
+
+  // purely to keep `db` around until we're done so it's not GCed if the user doesn't keep a ref
+  this._db = db
+
+  options = this._options = extend(defaultOptions, options)
+
+  this._keyEncoding   = options.keyEncoding   || options.encoding
+  this._valueEncoding = options.valueEncoding || options.encoding
+
+  if (typeof this._options.start != 'undefined')
+    this._options.start = util.encodeKey(this._options.start, this._options)
+  if (typeof this._options.end != 'undefined')
+    this._options.end = util.encodeKey(this._options.end, this._options)
+  if (typeof this._options.limit != 'number')
+    this._options.limit = -1
+
+  this._options.keyAsBuffer   = util.isKeyAsBuffer(this._options)
+
+  this._options.valueAsBuffer = util.isValueAsBuffer(this._options)
+
+  this._makeData = this._options.keys && this._options.values
+    ? makeKeyValueData : this._options.keys
+      ? makeKeyData : this._options.values
+        ? makeValueData : makeNoData
+
+  var self = this
+  if (!this._db.isOpen()) {
+    this._db.once('ready', function () {
+      if (!self._destroyed) {
+        self._iterator = iteratorFactory(self._options)
+      }
+    })
+  } else
+    this._iterator = iteratorFactory(this._options)
+}
+
+inherits(ReadStream, Readable)
+
+ReadStream.prototype._read = function read () {
+  var self = this
+  if (!self._db.isOpen()) {
+    return self._db.once('ready', function () { read.call(self) })
+  }
+  if (self._destroyed)
+    return
+ 
+  self._iterator.next(function(err, key, value) {
+    if (err || (key === undefined && value === undefined)) {
+      if (!err && !self._destroyed)
+        self.push(null)
+      return self._cleanup(err)
+    }
+
+    try {
+      value = self._makeData(key, value)
+    } catch (e) {
+      return self._cleanup(new EncodingError(e))
+    }
+    if (!self._destroyed)
+      self.push(value)
+  })
+}
+
+ReadStream.prototype._cleanup = function (err) {
+  if (this._destroyed)
+    return
+
+  this._destroyed = true
+
+  var self = this
+  if (err)
+    self.emit('error', err)
+
+  if (self._iterator) {
+    self._iterator.end(function () {
+      self._iterator = null
+      self.emit('close')
+    })
+  } else {
+    self.emit('close')
+  }
+}
+
+ReadStream.prototype.destroy = function () {
+  this._cleanup()
+}
+
+ReadStream.prototype.toString = function () {
+  return 'LevelUP.ReadStream'
+}
+
+module.exports = ReadStream
diff --git a/lib/util.js b/lib/util.js
new file mode 100644
index 0000000..e6b07e3
--- /dev/null
+++ b/lib/util.js
@@ -0,0 +1,182 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License
+ * <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var extend        = require('xtend')
+  , LevelUPError  = require('./errors').LevelUPError
+
+  , encodingNames = [
+        'hex'
+      , 'utf8'
+      , 'utf-8'
+      , 'ascii'
+      , 'binary'
+      , 'base64'
+      , 'ucs2'
+      , 'ucs-2'
+      , 'utf16le'
+      , 'utf-16le'
+    ]
+
+  , defaultOptions = {
+        createIfMissing : true
+      , errorIfExists   : false
+      , keyEncoding     : 'utf8'
+      , valueEncoding   : 'utf8'
+      , compression     : true
+    }
+
+  , leveldown
+
+  , encodings = (function () {
+      function isBinary (data) {
+        return data === undefined || data === null || Buffer.isBuffer(data)
+      }
+
+      var encodings = {}
+      encodings.utf8 = encodings['utf-8'] = {
+          encode : function (data) {
+            return isBinary(data) ? data : String(data)
+          }
+        , decode : function (data) {
+          return data
+          }
+        , buffer : false
+        , type   : 'utf8'
+      }
+      encodings.json = {
+          encode : JSON.stringify
+        , decode : JSON.parse
+        , buffer : false
+        , type   : 'json'
+      }
+      encodingNames.forEach(function (type) {
+        if (encodings[type])
+          return
+        encodings[type] = {
+            encode : function (data) {
+              return isBinary(data) ? data : new Buffer(data, type)
+            }
+          , decode : function (buffer) {
+              return process.browser ? buffer.toString(type) : buffer;
+            }
+          , buffer : true
+          , type   : type // useful for debugging purposes
+        }
+      })
+      return encodings
+    })()
+
+  , encodingOpts = (function () {
+      var eo = {}
+      encodingNames.forEach(function (e) {
+        eo[e] = { valueEncoding : e }
+      })
+      return eo
+    }())
+
+function copy (srcdb, dstdb, callback) {
+  srcdb.readStream()
+    .pipe(dstdb.writeStream())
+    .on('close', callback ? callback : function () {})
+    .on('error', callback ? callback : function (err) { throw err })
+}
+
+function getOptions (levelup, options) {
+  var s = typeof options == 'string' // just an encoding
+  if (!s && options && options.encoding && !options.valueEncoding)
+    options.valueEncoding = options.encoding
+  return extend(
+      (levelup && levelup.options) || {}
+    , s ? encodingOpts[options] || encodingOpts[defaultOptions.valueEncoding]
+        : options
+  )
+}
+
+function getLevelDOWN () {
+  if (leveldown)
+    return leveldown
+
+  var requiredVersion       = require('../package.json').devDependencies.leveldown
+    , missingLevelDOWNError = 'Could not locate LevelDOWN, try `npm install leveldown`'
+    , leveldownVersion
+
+  try {
+    leveldownVersion = require('leveldown/package').version
+  } catch (e) {
+    throw new LevelUPError(missingLevelDOWNError)
+  }
+
+  if (!require('semver').satisfies(leveldownVersion, requiredVersion)) {
+    throw new LevelUPError(
+        'Installed version of LevelDOWN ('
+      + leveldownVersion
+      + ') does not match required version ('
+      + requiredVersion
+      + ')'
+    )
+  }
+
+  try {
+    return leveldown = require('leveldown')
+  } catch (e) {
+    throw new LevelUPError(missingLevelDOWNError)
+  }
+}
+
+function dispatchError (levelup, error, callback) {
+  return typeof callback == 'function'
+    ? callback(error)
+    : levelup.emit('error', error)
+}
+
+function getKeyEncoder (options, op) {
+  var type = ((op && op.keyEncoding) || options.keyEncoding) || 'utf8'
+  return encodings[type] || type
+}
+
+function getValueEncoder (options, op) {
+  var type = (((op && (op.valueEncoding || op.encoding))
+      || options.valueEncoding || options.encoding)) || 'utf8'
+  return encodings[type] || type
+}
+
+function encodeKey (key, options, op) {
+  return getKeyEncoder(options, op).encode(key)
+}
+
+function encodeValue (value, options, op) {
+  return getValueEncoder(options, op).encode(value)
+}
+
+function decodeKey (key, options) {
+  return getKeyEncoder(options).decode(key)
+}
+
+function decodeValue (value, options) {
+  return getValueEncoder(options).decode(value)
+}
+
+function isValueAsBuffer (options, op) {
+  return getValueEncoder(options, op).buffer
+}
+
+function isKeyAsBuffer (options, op) {
+  return getKeyEncoder(options, op).buffer
+}
+
+module.exports = {
+    defaultOptions  : defaultOptions
+  , copy            : copy
+  , getOptions      : getOptions
+  , getLevelDOWN    : getLevelDOWN
+  , dispatchError   : dispatchError
+  , encodeKey       : encodeKey
+  , encodeValue     : encodeValue
+  , isValueAsBuffer : isValueAsBuffer
+  , isKeyAsBuffer   : isKeyAsBuffer
+  , decodeValue     : decodeValue
+  , decodeKey       : decodeKey
+}
diff --git a/lib/write-stream.js b/lib/write-stream.js
new file mode 100644
index 0000000..a55889f
--- /dev/null
+++ b/lib/write-stream.js
@@ -0,0 +1,178 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License
+ * <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var Stream       = require('stream').Stream
+  , inherits     = require('util').inherits
+  , extend       = require('xtend')
+  , bl           = require('bl')
+
+  , setImmediate = global.setImmediate || process.nextTick
+
+  , getOptions   = require('./util').getOptions
+
+  , defaultOptions = { type: 'put' }
+
+function WriteStream (options, db) {
+  if (!(this instanceof WriteStream))
+    return new WriteStream(options, db)
+
+  Stream.call(this)
+  this._options = extend(defaultOptions, getOptions(db, options))
+  this._db      = db
+  this._buffer  = []
+  this._status  = 'init'
+  this._end     = false
+  this.writable = true
+  this.readable = false
+
+  var self = this
+    , ready = function () {
+        if (!self.writable)
+          return
+        self._status = 'ready'
+        self.emit('ready')
+        self._process()
+      }
+
+  if (db.isOpen())
+    setImmediate(ready)
+  else
+    db.once('ready', ready)
+}
+
+inherits(WriteStream, Stream)
+
+WriteStream.prototype.write = function (data) {
+  if (!this.writable)
+    return false
+  this._buffer.push(data)
+  if (this._status != 'init')
+    this._processDelayed()
+  if (this._options.maxBufferLength &&
+      this._buffer.length > this._options.maxBufferLength) {
+    this._writeBlock = true
+    return false
+  }
+  return true
+}
+
+WriteStream.prototype.end = function (data) {
+  var self = this
+  if (data)
+    this.write(data)
+  setImmediate(function () {
+    self._end = true
+    self._process()
+  })
+}
+
+WriteStream.prototype.destroy = function () {
+  this.writable = false
+  this.end()
+}
+
+WriteStream.prototype.destroySoon = function () {
+  this.end()
+}
+
+WriteStream.prototype.add = function (entry) {
+  if (!entry.props)
+    return
+  if (entry.props.Directory)
+    entry.pipe(this._db.writeStream(this._options))
+  else if (entry.props.File || entry.File || entry.type == 'File')
+    this._write(entry)
+  return true
+}
+
+WriteStream.prototype._processDelayed = function () {
+  var self = this
+  setImmediate(function () {
+    self._process()
+  })
+}
+
+WriteStream.prototype._process = function () {
+  var buffer
+    , self = this
+
+    , cb = function (err) {
+        if (!self.writable)
+          return
+        if (self._status != 'closed')
+          self._status = 'ready'
+        if (err) {
+          self.writable = false
+          return self.emit('error', err)
+        }
+        self._process()
+      }
+
+  if (self._status != 'ready' && self.writable) {
+    if (self._buffer.length && self._status != 'closed')
+      self._processDelayed()
+    return
+  }
+
+  if (self._buffer.length && self.writable) {
+    self._status = 'writing'
+    buffer       = self._buffer
+    self._buffer = []
+
+    self._db.batch(buffer.map(function (d) {
+      return {
+          type          : d.type || self._options.type
+        , key           : d.key
+        , value         : d.value
+        , keyEncoding   : d.keyEncoding || self._options.keyEncoding
+        , valueEncoding : d.valueEncoding
+            || d.encoding
+            || self._options.valueEncoding
+      }
+    }), cb)
+
+    if (self._writeBlock) {
+      self._writeBlock = false
+      self.emit('drain')
+    }
+
+    // don't allow close until callback has returned
+    return
+  }
+
+  if (self._end && self._status != 'closed') {
+    self._status  = 'closed'
+    self.writable = false
+    self.emit('close')
+  }
+}
+
+WriteStream.prototype._write = function (entry) {
+  var key = entry.path || entry.props.path
+    , self = this
+
+  if (!key)
+    return
+
+  entry.pipe(bl(function (err, data) {
+    if (err) {
+      self.writable = false
+      return self.emit('error', err)
+    }
+
+    if (self._options.fstreamRoot &&
+        key.indexOf(self._options.fstreamRoot) > -1)
+      key = key.substr(self._options.fstreamRoot.length + 1)
+
+    self.write({ key: key, value: data.slice(0) })
+  }))
+}
+
+WriteStream.prototype.toString = function () {
+  return 'LevelUP.WriteStream'
+}
+
+module.exports = WriteStream
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..46858ba
--- /dev/null
+++ b/package.json
@@ -0,0 +1,73 @@
+{
+  "name": "levelup",
+  "description": "Fast & simple storage - a Node.js-style LevelDB wrapper",
+  "version": "0.18.5",
+  "contributors": [
+    "Rod Vagg <r at va.gg> (https://github.com/rvagg)",
+    "John Chesley <john at chesl.es> (https://github.com/chesles/)",
+    "Jake Verbaten <raynos2 at gmail.com> (https://github.com/raynos)",
+    "Dominic Tarr <dominic.tarr at gmail.com> (https://github.com/dominictarr)",
+    "Max Ogden <max at maxogden.com> (https://github.com/maxogden)",
+    "Lars-Magnus Skog <lars.magnus.skog at gmail.com> (https://github.com/ralphtheninja)",
+    "David Björklund <david.bjorklund at gmail.com> (https://github.com/kesla)",
+    "Julian Gruber <julian at juliangruber.com> (https://github.com/juliangruber)",
+    "Paolo Fragomeni <paolo at async.ly> (https://github.com/hij1nx)",
+    "Anton Whalley <anton.whalley at nearform.com> (https://github.com/No9)",
+    "Matteo Collina <matteo.collina at gmail.com> (https://github.com/mcollina)",
+    "Pedro Teixeira <pedro.teixeira at gmail.com> (https://github.com/pgte)",
+    "James Halliday <mail at substack.net> (https://github.com/substack)"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/rvagg/node-levelup.git"
+  },
+  "homepage": "https://github.com/rvagg/node-levelup",
+  "keywords": [
+    "leveldb",
+    "stream",
+    "database",
+    "db",
+    "store",
+    "storage",
+    "json"
+  ],
+  "main": "lib/levelup.js",
+  "dependencies": {
+    "bl": "~0.8.0",
+    "deferred-leveldown": "~0.2.0",
+    "errno": "~0.1.1",
+    "prr": "~0.0.0",
+    "readable-stream": "~1.0.26",
+    "semver": "~2.3.1",
+    "xtend": "~3.0.0"
+  },
+  "devDependencies": {
+    "leveldown": "~0.10.0",
+    "bustermove": "*",
+    "tap": "*",
+    "referee": "*",
+    "rimraf": "*",
+    "async": "*",
+    "fstream": "*",
+    "tar": "*",
+    "mkfiletree": "*",
+    "readfiletree": "*",
+    "slow-stream": ">=0.0.4",
+    "delayed": "*",
+    "boganipsum": "*",
+    "du": "*",
+    "memdown": "*",
+    "msgpack-js": "*"
+  },
+  "browser": {
+    "leveldown": false,
+    "leveldown/package": false,
+    "semver": false
+  },
+  "scripts": {
+    "test": "tap test/*-test.js --stderr",
+    "functionaltests": "node ./test/functional/fstream-test.js && node ./test/functional/binary-data-test.js && node ./test/functional/compat-test.js",
+    "alltests": "npm test && npm run-script functionaltests"
+  },
+  "license": "MIT"
+}
diff --git a/test/approximate-size-test.js b/test/approximate-size-test.js
new file mode 100644
index 0000000..8e48604
--- /dev/null
+++ b/test/approximate-size-test.js
@@ -0,0 +1,80 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('approximateSize()', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'approximateSize() works on empty database': function (done) {
+      this.openTestDatabase(function (db) {
+        db.approximateSize('a', 'z', function(err, size) {
+          refute(err) // sanity
+          assert.equals(size, 0)
+          done()
+        })
+      })
+    }
+
+  , 'approximateSize() work on none-empty database': function(done) {
+      var location = common.nextLocation()
+        , db
+
+      async.series(
+          [
+              function (callback) {
+                this.openTestDatabase(
+                    location
+                  , function (_db) {
+                    db = _db
+                    callback()
+                  }
+                )
+              }.bind(this)
+            , function (callback) {
+                var batch = []
+                  , i     = 0
+
+                for (; i < 10; ++i) {
+                  batch.push({
+                    type: 'put', key: String(i), value: 'afoovalue'
+                  })
+                }
+                db.batch(
+                    batch
+                  , { sync: true }
+                  , callback
+                )
+              }
+            , function (callback) {
+                // close db to make sure stuff gets written to disc
+                db.close(callback)
+              }
+            , function (callback) {
+                levelup(location, function (err, _db) {
+                  refute(err)
+                  db = _db
+                  callback()
+                })
+              }
+            , function (callback) {
+                db.approximateSize('0', '99', function(err, size) {
+                  refute(err) // sanity
+                  refute.equals(size, 0)
+                  callback()
+                })
+              }
+          ]
+        , done
+      )
+    }
+})
\ No newline at end of file
diff --git a/test/argument-checking-test.js b/test/argument-checking-test.js
new file mode 100644
index 0000000..e4a4962
--- /dev/null
+++ b/test/argument-checking-test.js
@@ -0,0 +1,122 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Argument checking', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'test get() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.get.bind(db)
+          , { name: 'ReadError', message: 'get() requires key and callback arguments' }
+          , 'no-arg get() throws'
+        )
+
+        assert.exception(
+            db.get.bind(db, 'foo')
+          , { name: 'ReadError', message: 'get() requires key and callback arguments' }
+          , 'callback-less, 1-arg get() throws'
+        )
+
+        assert.exception(
+            db.get.bind(db, 'foo', {})
+          , { name: 'ReadError', message: 'get() requires key and callback arguments' }
+          , 'callback-less, 2-arg get() throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test put() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.put.bind(db)
+          , { name: 'WriteError', message: 'put() requires key and value arguments' }
+          , 'no-arg put() throws'
+        )
+
+        assert.exception(
+            db.put.bind(db, 'foo')
+          , { name: 'WriteError', message: 'put() requires key and value arguments' }
+          , 'callback-less, 1-arg put() throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test del() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.del.bind(db)
+          , { name: 'WriteError', message: 'del() requires a key argument' }
+          , 'no-arg del() throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test approximateSize() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.approximateSize.bind(db)
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'no-arg approximateSize() throws'
+        )
+
+        assert.exception(
+            db.approximateSize.bind(db, 'foo')
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'callback-less, 1-arg approximateSize() throws'
+        )
+
+        assert.exception(
+            db.approximateSize.bind(db, 'foo', 'bar')
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'callback-less, 2-arg approximateSize() throws'
+        )
+
+        assert.exception(
+            db.approximateSize.bind(db, 'foo', 'bar', {})
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'callback-less, 3-arg approximateSize(), no cb throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test batch() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.batch.bind(db, null, {})
+          , { name: 'WriteError', message: 'batch() requires an array argument' }
+          , 'no-arg batch() throws'
+        )
+
+        assert.exception(
+            db.batch.bind(db, {})
+          , { name: 'WriteError', message: 'batch() requires an array argument' }
+          , '1-arg, no Array batch() throws'
+        )
+
+        done()
+      })
+    }
+})
diff --git a/test/batch-test.js b/test/batch-test.js
new file mode 100644
index 0000000..56a3d6d
--- /dev/null
+++ b/test/batch-test.js
@@ -0,0 +1,372 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var errors  = require('../lib/errors.js')
+  , async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('batch()', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'batch() with multiple puts': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(
+            [
+                { type: 'put', key: 'foo', value: 'afoovalue' }
+              , { type: 'put', key: 'bar', value: 'abarvalue' }
+              , { type: 'put', key: 'baz', value: 'abazvalue' }
+            ]
+          , function (err) {
+              refute(err)
+              async.forEach(
+                  ['foo', 'bar', 'baz']
+                , function (key, callback) {
+                    db.get(key, function (err, value) {
+                      refute(err)
+                      assert.equals(value, 'a' + key + 'value')
+                      callback()
+                    })
+                  }
+                , done
+              )
+            }
+        )
+      })
+    }
+
+  , 'batch() with multiple puts and deletes': function (done) {
+      this.openTestDatabase(function (db) {
+        async.series(
+            [
+                function (callback) {
+                  db.batch(
+                      [
+                          { type: 'put', key: '1', value: 'one' }
+                        , { type: 'put', key: '2', value: 'two' }
+                        , { type: 'put', key: '3', value: 'three' }
+                      ]
+                    , callback
+                  )
+                }
+              , function (callback) {
+                  db.batch(
+                      [
+                          { type: 'put', key: 'foo', value: 'afoovalue' }
+                        , { type: 'del', key: '1' }
+                        , { type: 'put', key: 'bar', value: 'abarvalue' }
+                        , { type: 'del', key: 'foo' }
+                        , { type: 'put', key: 'baz', value: 'abazvalue' }
+                      ]
+                    , callback
+                  )
+                }
+              , function (callback) {
+                  // these should exist
+                  async.forEach(
+                      ['2', '3', 'bar', 'baz']
+                    , function (key, callback) {
+                        db.get(key, function (err, value) {
+                          refute(err)
+                          refute.isNull(value)
+                          callback()
+                        })
+                      }
+                    , callback
+                  )
+                }
+              , function (callback) {
+                  // these shouldn't exist
+                  async.forEach(
+                      ['1', 'foo']
+                    , function (key, callback) {
+                        db.get(key, function (err, value) {
+                          assert(err)
+                          assert.isInstanceOf(err, errors.NotFoundError)
+                          refute(value)
+                          callback()
+                        })
+                      }
+                    , callback
+                  )
+                }
+            ]
+          , done
+        )
+      })
+    }
+
+  , 'batch() with chained interface': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put('1', 'one', function (err) {
+          refute(err)
+
+          db.batch()
+            .put('one', '1')
+            .del('two')
+            .put('three', '3')
+            .clear()
+            .del('1')
+            .put('2', 'two')
+            .put('3', 'three')
+            .del('3')
+            .write(function (err) {
+              refute(err)
+
+              async.forEach(
+                  [ 'one', 'three', '1', '2', '3']
+                , function (key, callback) {
+                    db.get(key, function (err) {
+                      if ([ 'one', 'three', '1', '3' ].indexOf(key) > -1)
+                        assert(err)
+                      else
+                        refute(err)
+                      callback()
+                    })
+                  }
+                , done
+              )
+            })
+        })
+      })
+    }
+
+  , 'batch() with can manipulate data from put()': function (done) {
+      // checks encoding and whatnot
+      this.openTestDatabase(function (db) {
+        async.series(
+            [
+                db.put.bind(db, '1', 'one')
+              , db.put.bind(db, '2', 'two')
+              , db.put.bind(db, '3', 'three')
+              , function (callback) {
+                  db.batch(
+                      [
+                          { type: 'put', key: 'foo', value: 'afoovalue' }
+                        , { type: 'del', key: '1' }
+                        , { type: 'put', key: 'bar', value: 'abarvalue' }
+                        , { type: 'del', key: 'foo' }
+                        , { type: 'put', key: 'baz', value: 'abazvalue' }
+                      ]
+                    , callback
+                  )
+                }
+              , function (callback) {
+                  // these should exist
+                  async.forEach(
+                      ['2', '3', 'bar', 'baz']
+                    , function (key, callback) {
+                        db.get(key, function (err, value) {
+                          refute(err)
+                          refute.isNull(value)
+                          callback()
+                        })
+                      }
+                    , callback
+                  )
+                }
+              , function (callback) {
+                  // these shouldn't exist
+                  async.forEach(
+                      ['1', 'foo']
+                    , function (key, callback) {
+                        db.get(key, function (err, value) {
+                          assert(err)
+                          assert.isInstanceOf(err, errors.NotFoundError)
+                          refute(value)
+                          callback()
+                        })
+                      }
+                    , callback
+                  )
+                }
+            ]
+          , done
+        )
+      })
+    }
+
+  , 'batch() data can be read with get() and del()': function (done) {
+      this.openTestDatabase(function (db) {
+        async.series(
+            [
+                function (callback) {
+                  db.batch(
+                      [
+                          { type: 'put', key: '1', value: 'one' }
+                        , { type: 'put', key: '2', value: 'two' }
+                        , { type: 'put', key: '3', value: 'three' }
+                      ]
+                    , callback
+                  )
+                }
+              , db.del.bind(db, '1', 'one')
+              , function (callback) {
+                  // these should exist
+                  async.forEach(
+                      ['2', '3']
+                    , function (key, callback) {
+                        db.get(key, function (err, value) {
+                          refute(err)
+                          refute.isNull(value)
+                          callback()
+                        })
+                      }
+                    , callback
+                  )
+                }
+              , function (callback) {
+                  // this shouldn't exist
+                  db.get('1', function (err, value) {
+                    assert(err)
+                    assert.isInstanceOf(err, errors.NotFoundError)
+                    refute(value)
+                    callback()
+                  })
+                }
+            ]
+          , done
+        )
+      })
+    }
+
+  , 'chained batch() arguments': {
+        'setUp': function (done) {
+          this.openTestDatabase(function (db) {
+            this.db = db
+            this.batch = db.batch()
+            done()
+          }.bind(this))
+        }
+
+      , 'test batch#put() with missing `value`': function () {
+          // value = undefined
+          assert.exception(this.batch.put.bind(this.batch, 'foo1'), function (err) {
+            console.log('err.name', err.name, 'err.message', err.message)
+            if (err.name != 'WriteError')
+              return false
+            if ('value cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+
+          // value = null
+          assert.exception(this.batch.put.bind(this.batch, 'foo1', null), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('value cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+        }
+
+      , 'test batch#put() with missing `key`': function () {
+          // key = undefined
+          assert.exception(this.batch.put.bind(this.batch, undefined, 'foo1'), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('key cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+
+          // key = null
+          assert.exception(this.batch.put.bind(this.batch, null, 'foo1'), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('key cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+        }
+
+      , 'test batch#put() with missing `key` and `value`': function () {
+          // undefined
+          assert.exception(this.batch.put.bind(this.batch), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('key cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+
+          // null
+          assert.exception(this.batch.put.bind(this.batch, null, null), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('key cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+        }
+
+      , 'test batch#del() with missing `key`': function () {
+          // key = undefined
+          assert.exception(this.batch.del.bind(this.batch, undefined, 'foo1'), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('key cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+
+          // key = null
+          assert.exception(this.batch.del.bind(this.batch, null, 'foo1'), function (err) {
+            if (err.name != 'WriteError')
+              return false
+            if ('key cannot be `null` or `undefined`' != err.message)
+              return false
+            return true
+          })
+        }
+
+      , 'test batch#write() with no callback': function () {
+          this.batch.write() // should not cause an error with no cb
+        }
+
+      , 'test batch operations after write()': {
+            'setUp': function (done) {
+              this.batch.put('foo', 'bar').put('boom', 'bang').del('foo').write(done)
+              this.verify = function (cb) {
+                assert.exception(cb, function (err) {
+                  if (err.name != 'WriteError')
+                    return false
+                  if ('write() already called on this batch' != err.message)
+                    return false
+                  return true
+                })
+              }
+            }
+
+          , 'test put()': function () {
+              this.verify(function () {
+                this.batch.put('whoa', 'dude')
+              }.bind(this))
+            }
+
+          , 'test del()': function () {
+              this.verify(function () {
+                this.batch.del('foo')
+              }.bind(this))
+            }
+
+          , 'test clear()': function () {
+              this.verify(function () {
+                this.batch.clear()
+              }.bind(this))
+            }
+
+          , 'test write()': function () {
+              this.verify(function () {
+                this.batch.write()
+              }.bind(this))
+            }
+        }
+    }
+})
\ No newline at end of file
diff --git a/test/benchmarks/README.md b/test/benchmarks/README.md
new file mode 100644
index 0000000..ef6d0bf
--- /dev/null
+++ b/test/benchmarks/README.md
@@ -0,0 +1,42 @@
+# LevelUP Benchmarks
+
+## Using
+
+In this directory:
+
+```sh
+$ npm install
+$ node ./
+```
+
+## Philosophy
+
+LevelUP's primary goal is to provide a ***safe***, ***useful*** and ***natural*** Node.js API on top of LevelDB. Performance is important but not at the expense of safety or the utility of the API.
+
+The goal of this benchmark suite is to:
+
+  * Pinpoint areas where performance can/should be improved
+  * Prevent performance regressions between releases
+  * Compare the suitability of LevelUP/LevelDB as a persistent storage mechanism for Node.js (e,g. what do you gain in terms of performance by sacrificing a more rich query interface available in something like SQLite?)
+
+## About
+
+Currently, we the benchmarks run for the current version of LevelUP (i.e. `require('../../')`), the version of LevelUP currently in npm, [Leveled](https://github.com/juliangruber/node-leveled) and [node-sqlite3](https://github.com/developmentseed/node-sqlite3/). Benchmarks for LevelUP with compression disabled can also be run by editing the *engins/index.js* file.
+
+**Leveled** is a minimal binding that is mostly implemented in C++ with few options, only `String` data-types and minimal safety checks. Therefore it's an excellent baseline for performance that LevelUP can aspire to.
+
+**SQLite3** is included to see how an alternative persistent k/v store can perform. The comparison is only fair when considering a simple k/v store and in this regard the main SQLite3 binding performs poorly.
+
+## How does it work?
+
+The main benchmark runner is *./index.js*, it uses [Benchmark.js](http://benchmarkjs.com/) to do the heavy lifting and measuring. "Engines" (i.e. databases) are specified in the *./engines/* directory, to add a new engine/database simply follow the pattern used for the existing ones. The benchmark tests themselves are stored in the *./tests/* directory. *./tests/index.js* lists them all and uses a simple `require()` to pull them in from the various files. The tests index maps the engine  [...]
+
+Tests without a mapping for a given engine are simply not invoked for that engine.
+
+Individual tests can be singled out by prepending `'=>'` to the *name* of the test. The runner will then single that test out and only run it with the various engines.
+
+## Contributing
+
+Yes please! I'd love help making these more rigorous and meaningful. Please contribute if you see something you can add.
+
+Bother *@rvagg* if you have trouble working any of this out.
\ No newline at end of file
diff --git a/test/benchmarks/engines/index.js b/test/benchmarks/engines/index.js
new file mode 100644
index 0000000..22a1caf
--- /dev/null
+++ b/test/benchmarks/engines/index.js
@@ -0,0 +1,19 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+module.exports.LevelUP = require('./levelup')
+module.exports.LevelUP.color = 'green'
+
+module.exports['LevelUP (release)'] = require('./levelup-release')
+module.exports['LevelUP (release)'].color = 'yellow'
+
+//module.exports['LevelUP (no Snappy)'] = require('./levelup-nosnappy')
+//module.exports['LevelUP (no Snappy)'].color = 'magenta'
+
+module.exports.Leveled = require('./leveled')
+module.exports.Leveled.color = 'cyan'
+
+module.exports.SQLite3 = require('./sqlite3')
+module.exports.SQLite3.color = 'blue'
\ No newline at end of file
diff --git a/test/benchmarks/engines/leveled.js b/test/benchmarks/engines/leveled.js
new file mode 100644
index 0000000..1f3e6b6
--- /dev/null
+++ b/test/benchmarks/engines/leveled.js
@@ -0,0 +1,21 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var leveled = require('leveled')
+
+  , createDb = function (location, callback) {
+      var db = leveled(location) // no callback, is it sync?
+      setTimeout(callback.bind(null, null, db), 50)
+    }
+
+  , closeDb = function (db, callback) {
+      // has no close()
+      callback()
+    }
+
+module.exports = {
+    createDb : createDb
+  , closeDb  : closeDb
+}
\ No newline at end of file
diff --git a/test/benchmarks/engines/levelup-nosnappy.js b/test/benchmarks/engines/levelup-nosnappy.js
new file mode 100644
index 0000000..350997f
--- /dev/null
+++ b/test/benchmarks/engines/levelup-nosnappy.js
@@ -0,0 +1,21 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../../../')
+
+  , createDb = function (location, callback) {
+      levelup(location, { createIfMissing: true, errorIfExists: true, compression: false }, function (err, db) {
+        setTimeout(callback.bind(null, err, db), 50)
+      })
+    }
+
+  , closeDb = function (db, callback) {
+      db.close(callback)
+    }
+
+module.exports = {
+    createDb : createDb
+  , closeDb  : closeDb
+}
\ No newline at end of file
diff --git a/test/benchmarks/engines/levelup-release.js b/test/benchmarks/engines/levelup-release.js
new file mode 100644
index 0000000..b7f38c1
--- /dev/null
+++ b/test/benchmarks/engines/levelup-release.js
@@ -0,0 +1,21 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('levelup')
+
+  , createDb = function (location, callback) {
+      levelup(location, { createIfMissing: true, errorIfExists: true }, function (err, db) {
+        setTimeout(callback.bind(null, err, db), 50)
+      })
+    }
+
+  , closeDb = function (db, callback) {
+      db.close(callback)
+    }
+
+module.exports = {
+    createDb : createDb
+  , closeDb  : closeDb
+}
\ No newline at end of file
diff --git a/test/benchmarks/engines/levelup.js b/test/benchmarks/engines/levelup.js
new file mode 100644
index 0000000..dc59b6c
--- /dev/null
+++ b/test/benchmarks/engines/levelup.js
@@ -0,0 +1,21 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../../../')
+
+  , createDb = function (location, callback) {
+      levelup(location, { createIfMissing: true, errorIfExists: true }, function (err, db) {
+        setTimeout(callback.bind(null, err, db), 50)
+      })
+    }
+
+  , closeDb = function (db, callback) {
+      db.close(callback)
+    }
+
+module.exports = {
+    createDb : createDb
+  , closeDb  : closeDb
+}
\ No newline at end of file
diff --git a/test/benchmarks/engines/sqlite3.js b/test/benchmarks/engines/sqlite3.js
new file mode 100644
index 0000000..3fc0dc5
--- /dev/null
+++ b/test/benchmarks/engines/sqlite3.js
@@ -0,0 +1,26 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var sqlite3 = require('sqlite3')
+
+  , createDb = function (location, callback) {
+      var db = new sqlite3.Database(location, function (err) {
+        if (err) return callback(err)
+        db.run("CREATE TABLE bench (key VARCHAR(32), value TEXT)", function (err) {
+          if (err) return callback(err)
+          setTimeout(callback.bind(null, null, db), 50)
+        })
+      })
+    }
+
+  , closeDb = function (db, callback) {
+      db.close() // does it have a callback?
+      setTimeout(callback, 50)
+    }
+
+module.exports = {
+    createDb : createDb
+  , closeDb  : closeDb
+}
\ No newline at end of file
diff --git a/test/benchmarks/index.js b/test/benchmarks/index.js
new file mode 100644
index 0000000..2d372a5
--- /dev/null
+++ b/test/benchmarks/index.js
@@ -0,0 +1,97 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+const DB_ROOT = __dirname
+
+var path = require('path')
+  , Benchmark = require('benchmark')
+  , rimraf = require('rimraf')
+  , async = require('async')
+  , engines = require('./engines/')
+  , tests = require('./tests/')
+
+  , dbidx = 0
+
+  , printableEngineName = function (engineName) {
+      var len = Object.keys(engines).reduce(function (m, c) { return Math.max(c.length, m) }, 0)
+      while (engineName.length < len) engineName += ' '
+      return engineName
+    }
+
+  , mklocation = function () {
+      return path.join(DB_ROOT, '_benchdb_' + dbidx++)
+    }
+
+  , mkdb = function (engine, location, callback) {
+      rimraf(location, engine.createDb.bind(null, location, callback))
+    }
+
+  , rmdb = function (engine, db, location, callback) {
+      engine.closeDb(db, rimraf.bind(null, location, callback))
+    }
+
+  , run = function (db, name, fn, color, cb) {
+      var exec = function () {
+        new Benchmark(name, {
+          'defer': true,
+          'fn': function  (deferred) {
+            fn(db, deferred.resolve.bind(deferred))
+          }
+        })
+        .on('complete', function(event) {
+          console.log(String(event.target)[color].bold)
+          cb()
+        })
+        .run({ async: true })
+      }
+
+      if (fn.setup) {
+        fn.setup(db, function (err) {
+          if (err) return cb(err)
+          exec()
+        })
+      } else
+        exec()
+    }
+
+  , runTest = function (testName, callback) {
+      async.forEachSeries(
+          Object.keys(engines)
+        , function (engineKey, callback) {
+            var engine = engines[engineKey]
+            var location = mklocation()
+            mkdb(engine, location, function (err, db) {
+              if (err) return callback(err)
+              if (!tests[testName][engineKey]) { console.log('Skipping for', testName, engineKey); return callback() }
+              run(
+                  db
+                , printableEngineName(engineKey) + ' ' + testName
+                , tests[testName][engineKey]
+                , engine.color
+                , function (err) {
+                    rmdb(engine, db, location, function (_err) {
+                      callback(err || _err)
+                    })
+                  }
+              )
+            })
+          }
+        , function () {
+            console.log()
+            callback.apply(null, arguments)
+          }
+      )
+    }
+
+  , focusKey = Object.keys(tests).filter(function (k) { return (/\=>/).test(k) })
+
+if (focusKey.length) {
+  var focusTest = tests[focusKey[0]]
+  tests = {}
+  tests[focusKey[0]] = focusTest
+}
+
+require('colors')
+async.forEachSeries(Object.keys(tests), runTest)
\ No newline at end of file
diff --git a/test/benchmarks/package.json b/test/benchmarks/package.json
new file mode 100644
index 0000000..11f0ad9
--- /dev/null
+++ b/test/benchmarks/package.json
@@ -0,0 +1,16 @@
+{
+    "name": "levelup-benchmarks"
+  , "version": "0.0.0"
+  , "description": "benchmarks for LevelUP"
+  , "main": "index.js"
+  , "private": true
+  , "dependencies": {
+        "benchmark": "~1.0.0"
+      , "colors": "~0.6.0-1"
+      , "rimraf": "~2.0.2"
+      , "async": "~0.1.22"
+      , "levelup": "*"
+      , "leveled": "*"
+      , "sqlite3": "*"
+    }
+}
\ No newline at end of file
diff --git a/test/benchmarks/stream-bench.js b/test/benchmarks/stream-bench.js
new file mode 100644
index 0000000..a964020
--- /dev/null
+++ b/test/benchmarks/stream-bench.js
@@ -0,0 +1,43 @@
+const levelup = require('../../')
+    , crypto  = require('crypto')
+    , srcdb   = levelup('/tmp/source.db')
+    , dstdb   = levelup('/tmp/destination.db')
+
+    , batch   = 10000
+    , total   = 200000
+ 
+function fillBatch (start, callback) {
+  var b = []
+  for (var i = start; i < start + batch; i++) {
+    b.push({ type: 'put', key: i, value: crypto.randomBytes(100) })
+  }
+  srcdb.batch(b, callback)
+}
+
+function populate (start, callback) {
+  if (start > total)
+    return callback()
+  fillBatch(start, function (err) {
+    if (err) throw err
+    populate(start + batch, callback)
+  })
+}
+
+srcdb.on('ready', function () {
+  var start = Date.now()
+
+  populate(0, function () {
+    var batchTime = Date.now() - start
+
+    console.log('--------------------------------------------------------------')
+    console.log('Filled source! Took', batchTime + 'ms, streaming to destination...')
+
+    start = Date.now()
+    srcdb.createReadStream()
+      .on('end', function () {
+        var copyTime = Date.now() - start
+        console.log('Done! Took', copyTime + 'ms,', Math.round((copyTime / batchTime) * 100) + '% of batch time')
+      })
+      .pipe(dstdb.createWriteStream())
+  })
+})
diff --git a/test/benchmarks/tests/batch_int_string_x100000_leveled.js b/test/benchmarks/tests/batch_int_string_x100000_leveled.js
new file mode 100644
index 0000000..5b74c81
--- /dev/null
+++ b/test/benchmarks/tests/batch_int_string_x100000_leveled.js
@@ -0,0 +1 @@
+module.exports = require('./batch_int_string_x1000_leveled').fn.bind(null, 100000)
\ No newline at end of file
diff --git a/test/benchmarks/tests/batch_int_string_x100000_levelup.js b/test/benchmarks/tests/batch_int_string_x100000_levelup.js
new file mode 100644
index 0000000..cd8e481
--- /dev/null
+++ b/test/benchmarks/tests/batch_int_string_x100000_levelup.js
@@ -0,0 +1 @@
+module.exports = require('./batch_int_string_x1000_levelup').fn.bind(null, 100000)
\ No newline at end of file
diff --git a/test/benchmarks/tests/batch_int_string_x1000_leveled.js b/test/benchmarks/tests/batch_int_string_x1000_leveled.js
new file mode 100644
index 0000000..2e98440
--- /dev/null
+++ b/test/benchmarks/tests/batch_int_string_x1000_leveled.js
@@ -0,0 +1,20 @@
+var fn = function (puts, db, cb) {
+  var after = function (err) {
+        if (err) throw err
+        cb()
+      }
+    , batch = db.batch()
+
+  if (this.cycle == null) this.cycle = 0
+  else this.cycle++
+
+  for (var i = 0; i < puts; i++)
+    batch.put(
+        String(this.cycle * puts + i)
+      , "It'll be top end no worries stands out like a bushie. It'll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."
+    )
+  batch.write(after)
+}
+
+module.exports = fn.bind(null, 1000)
+module.exports.fn = fn
\ No newline at end of file
diff --git a/test/benchmarks/tests/batch_int_string_x1000_levelup.js b/test/benchmarks/tests/batch_int_string_x1000_levelup.js
new file mode 100644
index 0000000..4f559ee
--- /dev/null
+++ b/test/benchmarks/tests/batch_int_string_x1000_levelup.js
@@ -0,0 +1,21 @@
+var fn = function (puts, db, cb) {
+  var after = function (err) {
+        if (err) throw err
+        cb()
+      }
+    , data = []
+
+  if (this.cycle == null) this.cycle = 0
+  else this.cycle++
+
+  for (var i = 0; i < puts; i++)
+    data.push({
+            type: 'put'
+          , key: this.cycle * puts + i
+          , value: "It'll be top end no worries stands out like a bushie. It'll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."
+        })
+  db.batch(data, after)
+}
+
+module.exports = fn.bind(null, 1000)
+module.exports.fn = fn
\ No newline at end of file
diff --git a/test/benchmarks/tests/get_int_string_x1000_leveled.js b/test/benchmarks/tests/get_int_string_x1000_leveled.js
new file mode 100644
index 0000000..68b49a4
--- /dev/null
+++ b/test/benchmarks/tests/get_int_string_x1000_leveled.js
@@ -0,0 +1,14 @@
+var setupFn = function (count, db, cb) {
+  var batch = db.batch()
+  for (var i = 0; i < count; i++)
+    batch.put(
+        String(i)
+      , "It'll be top end no worries stands out like a bushie. It'll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."
+    )
+
+  batch.write(cb)
+}
+
+module.exports = require('./get_int_string_x1000_levelup').bind(null) // bind() to make a new function to put .setup on
+module.exports.setupFn = setupFn
+module.exports.setup = setupFn.bind(null, 1000)
\ No newline at end of file
diff --git a/test/benchmarks/tests/get_int_string_x1000_levelup.js b/test/benchmarks/tests/get_int_string_x1000_levelup.js
new file mode 100644
index 0000000..22c1bf8
--- /dev/null
+++ b/test/benchmarks/tests/get_int_string_x1000_levelup.js
@@ -0,0 +1,29 @@
+var setupFn = function (count, db, cb) {
+      var data = []
+
+      for (var i = 0; i < count; i++)
+        data.push({
+            type: 'put'
+          , key: String(i)
+          , value: "It'll be top end no worries stands out like a bushie. It'll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."
+        })
+
+      db.batch(data, cb)
+    }
+
+  , fn = function (count, db, cb) {
+      var received = 0
+        , after = function (err) {
+            if (err) throw err
+            if (++received == count) cb()
+          }
+
+      for (var i = 0; i < count; i++)
+        db.get(String(i), after)
+    }
+
+
+module.exports = fn.bind(null, 1000)
+module.exports.fn = fn
+module.exports.setup = setupFn.bind(null, 1000)
+module.exports.setupFn = setupFn
\ No newline at end of file
diff --git a/test/benchmarks/tests/get_int_string_x1000_sqlite3.js b/test/benchmarks/tests/get_int_string_x1000_sqlite3.js
new file mode 100644
index 0000000..5316856
--- /dev/null
+++ b/test/benchmarks/tests/get_int_string_x1000_sqlite3.js
@@ -0,0 +1,33 @@
+var async = require('async')
+
+  , setupFn = function (count, db, cb) {
+      var queue = async.queue(function (key, callback) {
+        db.exec(
+            'INSERT INTO bench VALUES('
+          + key
+          + ', "It\'ll be top end no worries stands out like a bushie. It\'ll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."'
+          + ')'
+          , callback
+        )
+      }, 20)
+      queue.drain = cb
+      for (var i = 0; i < count; i++)
+        queue.push(String(i))
+    }
+
+  , fn = function (count, db, cb) {
+      var received = 0
+        , after = function (err) {
+            if (err) throw err
+            if (++received == count) cb()
+          }
+
+      for (var i = 0; i < count; i++)
+        db.get('SELECT value FROM bench WHERE key = "' + i + '"', after)
+    }
+
+
+module.exports = fn.bind(null, 1000)
+module.exports.fn = fn
+module.exports.setup = setupFn.bind(null, 1000)
+module.exports.setupFn = setupFn
\ No newline at end of file
diff --git a/test/benchmarks/tests/get_int_string_x10_leveled.js b/test/benchmarks/tests/get_int_string_x10_leveled.js
new file mode 100644
index 0000000..913df45
--- /dev/null
+++ b/test/benchmarks/tests/get_int_string_x10_leveled.js
@@ -0,0 +1,2 @@
+module.exports = require('./get_int_string_x1000_levelup').fn.bind(null, 10)
+module.exports.setup = require('./get_int_string_x1000_leveled').setupFn.bind(null, 10)
\ No newline at end of file
diff --git a/test/benchmarks/tests/get_int_string_x10_levelup.js b/test/benchmarks/tests/get_int_string_x10_levelup.js
new file mode 100644
index 0000000..3b5f6ea
--- /dev/null
+++ b/test/benchmarks/tests/get_int_string_x10_levelup.js
@@ -0,0 +1,2 @@
+module.exports = require('./get_int_string_x1000_levelup').fn.bind(null, 10)
+module.exports.setup = require('./get_int_string_x1000_levelup').setupFn.bind(null, 10)
\ No newline at end of file
diff --git a/test/benchmarks/tests/get_int_string_x10_sqlite3.js b/test/benchmarks/tests/get_int_string_x10_sqlite3.js
new file mode 100644
index 0000000..0b40d50
--- /dev/null
+++ b/test/benchmarks/tests/get_int_string_x10_sqlite3.js
@@ -0,0 +1,2 @@
+module.exports = require('./get_int_string_x1000_sqlite3').fn.bind(null, 10)
+module.exports.setup = require('./get_int_string_x1000_sqlite3').setupFn.bind(null, 10)
\ No newline at end of file
diff --git a/test/benchmarks/tests/index.js b/test/benchmarks/tests/index.js
new file mode 100644
index 0000000..bde6889
--- /dev/null
+++ b/test/benchmarks/tests/index.js
@@ -0,0 +1,94 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+/*
+ * Prefix a test name with '=>' to have only that test run
+ */
+
+module.exports = {
+    'put(int, string) x 10': {
+        'LevelUP'             : require('./put_int_string_x10_levelup')
+      , 'LevelUP (release)'   : require('./put_int_string_x10_levelup')
+      , 'LevelUP (no Snappy)' : require('./put_int_string_x10_levelup')
+      , 'Leveled'             : require('./put_int_string_x10_leveled')
+      //, 'SQLite3'             : require('./put_int_string_x10_sqlite3')
+    }
+
+  , 'put(int, string) x 1000': {
+        'LevelUP'             : require('./put_int_string_x1000_levelup')
+      , 'LevelUP (release)'   : require('./put_int_string_x1000_levelup')
+      , 'LevelUP (no Snappy)' : require('./put_int_string_x1000_levelup')
+      , 'Leveled'             : require('./put_int_string_x1000_leveled')
+      //, 'SQLite3'             : require('./put_int_string_x1000_sqlite3')
+    }
+
+  , 'put(int, string) x 100,000': {
+        'LevelUP'             : require('./put_int_string_x100000_levelup')
+      , 'LevelUP (release)'   : require('./put_int_string_x100000_levelup')
+      , 'LevelUP (no Snappy)' : require('./put_int_string_x100000_levelup')
+      , 'Leveled'             : require('./put_int_string_x100000_leveled')
+      // too slow , 'SQLite3'             : require('./put_int_string_x100000_sqlite3')
+    }
+
+  , 'get(int):string x 10': {
+        'LevelUP'             : require('./get_int_string_x10_levelup')
+      , 'LevelUP (release)'   : require('./get_int_string_x10_levelup')
+      , 'LevelUP (no Snappy)' : require('./get_int_string_x10_levelup')
+      , 'Leveled'             : require('./get_int_string_x10_leveled')
+      //, 'SQLite3'             : require('./get_int_string_x10_sqlite3')
+    }
+
+  , 'get(int):string x 1000': {
+        'LevelUP'             : require('./get_int_string_x1000_levelup')
+      , 'LevelUP (release)'   : require('./get_int_string_x1000_levelup')
+      , 'LevelUP (no Snappy)' : require('./get_int_string_x1000_levelup')
+      , 'Leveled'             : require('./get_int_string_x1000_leveled')
+      //, 'SQLite3'             : require('./get_int_string_x1000_sqlite3')
+    }
+
+  , 'batch(int, string) x 1000': {
+        'LevelUP'             : require('./batch_int_string_x1000_levelup')
+      , 'LevelUP (release)'   : require('./batch_int_string_x1000_levelup')
+      , 'LevelUP (no Snappy)' : require('./batch_int_string_x1000_levelup')
+      , 'Leveled'             : require('./batch_int_string_x1000_leveled')
+    }
+
+  , 'batch(int, string) x 100,000': {
+        'LevelUP'             : require('./batch_int_string_x100000_levelup')
+      , 'LevelUP (release)'   : require('./batch_int_string_x100000_levelup')
+      , 'LevelUP (no Snappy)' : require('./batch_int_string_x100000_levelup')
+      , 'Leveled'             : require('./batch_int_string_x100000_leveled')
+    }
+
+  , 'readStream x 10': {
+        'LevelUP'             : require('./readStream_x10_levelup')
+      , 'LevelUP (release)'   : require('./readStream_x10_levelup')
+      , 'LevelUP (no Snappy)' : require('./readStream_x10_levelup')
+    }
+
+  , 'readStream x 100': {
+        'LevelUP'             : require('./readStream_x100_levelup')
+      , 'LevelUP (release)'   : require('./readStream_x100_levelup')
+      , 'LevelUP (no Snappy)' : require('./readStream_x100_levelup')
+    }
+
+  , 'readStream x 1,000': {
+        'LevelUP'             : require('./readStream_x1000_levelup')
+      , 'LevelUP (release)'   : require('./readStream_x1000_levelup')
+      , 'LevelUP (no Snappy)' : require('./readStream_x1000_levelup')
+    }
+
+  , 'readStream x 10,000': {
+        'LevelUP'             : require('./readStream_x10000_levelup')
+      , 'LevelUP (release)'   : require('./readStream_x10000_levelup')
+      , 'LevelUP (no Snappy)' : require('./readStream_x10000_levelup')
+    }
+
+  , 'readStream x 100,000': {
+        'LevelUP'             : require('./readStream_x100000_levelup')
+      , 'LevelUP (release)'   : require('./readStream_x100000_levelup')
+      , 'LevelUP (no Snappy)' : require('./readStream_x100000_levelup')
+    }
+}
diff --git a/test/benchmarks/tests/put_int_string_x100000_leveled.js b/test/benchmarks/tests/put_int_string_x100000_leveled.js
new file mode 100644
index 0000000..31c20b3
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x100000_leveled.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x100000_levelup')
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x100000_levelup.js b/test/benchmarks/tests/put_int_string_x100000_levelup.js
new file mode 100644
index 0000000..2fa883d
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x100000_levelup.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x1000_levelup').fn.bind(null, 100000)
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x100000_sqlite3.js b/test/benchmarks/tests/put_int_string_x100000_sqlite3.js
new file mode 100644
index 0000000..d65c35f
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x100000_sqlite3.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x1000_sqlite3').fn.bind(null, 100000)
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x1000_leveled.js b/test/benchmarks/tests/put_int_string_x1000_leveled.js
new file mode 100644
index 0000000..dcbe622
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x1000_leveled.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x1000_levelup')
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x1000_levelup.js b/test/benchmarks/tests/put_int_string_x1000_levelup.js
new file mode 100644
index 0000000..2525ccd
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x1000_levelup.js
@@ -0,0 +1,20 @@
+var fn = function (puts, db, cb) {
+  var received = 0
+    , after = function (err) {
+        if (err) throw err
+        if (++received == puts) cb()
+      }
+
+  if (this.cycle == null) this.cycle = 0
+  else this.cycle++
+
+  for (var i = 0; i < puts; i++)
+    db.put(
+        String(this.cycle * puts + i)
+      , "It'll be top end no worries stands out like a bushie. It'll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."
+      , after
+    )
+}
+
+module.exports = fn.bind(null, 1000)
+module.exports.fn = fn
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x1000_sqlite3.js b/test/benchmarks/tests/put_int_string_x1000_sqlite3.js
new file mode 100644
index 0000000..64af34b
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x1000_sqlite3.js
@@ -0,0 +1,22 @@
+var fn = function (puts, db, cb) {
+  var received = 0
+    , after = function (err) {
+        if (err) throw err
+        if (++received == puts) cb()
+      }
+
+  if (this.cycle == null) this.cycle = 0
+  else this.cycle++
+
+  for (var i = 0; i < puts; i++)
+    db.exec(
+        'INSERT INTO bench VALUES('
+      + String(this.cycle * puts + i)
+      + ', "It\'ll be top end no worries stands out like a bushie. It\'ll be cream no dramas flat out like a rotten. As busy as a slabs bloody built like a stonkered. Get a dog up ya oldies no dramas lets get some bottle-o. Built like a schooner as busy as a big smoke. You little ripper ute my you little ripper dag."'
+      + ')'
+      , after
+    )
+}
+
+module.exports = fn.bind(null, 1000)
+module.exports.fn = fn
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x10_leveled.js b/test/benchmarks/tests/put_int_string_x10_leveled.js
new file mode 100644
index 0000000..1832a4a
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x10_leveled.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x10_levelup')
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x10_levelup.js b/test/benchmarks/tests/put_int_string_x10_levelup.js
new file mode 100644
index 0000000..c72cf4f
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x10_levelup.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x1000_levelup').fn.bind(null, 10)
\ No newline at end of file
diff --git a/test/benchmarks/tests/put_int_string_x10_sqlite3.js b/test/benchmarks/tests/put_int_string_x10_sqlite3.js
new file mode 100644
index 0000000..a861217
--- /dev/null
+++ b/test/benchmarks/tests/put_int_string_x10_sqlite3.js
@@ -0,0 +1 @@
+module.exports = require('./put_int_string_x1000_sqlite3').fn.bind(null, 10)
\ No newline at end of file
diff --git a/test/benchmarks/tests/readStream_x100000_levelup.js b/test/benchmarks/tests/readStream_x100000_levelup.js
new file mode 100644
index 0000000..1baae7f
--- /dev/null
+++ b/test/benchmarks/tests/readStream_x100000_levelup.js
@@ -0,0 +1,17 @@
+
+var setupFn = function (count, db, cb) {
+      var doWrites = function() {
+        if(--count === 0) return cb()
+        db.put("aa" + count, "bb" + count, doWrites)
+      }
+      doWrites()
+    }
+
+  , fn = function (db, cb) {
+      db.createReadStream().on("end", cb)
+    }
+
+module.exports = fn
+module.exports.fn = fn
+module.exports.setup = setupFn.bind(null, 100000)
+module.exports.setupFn = setupFn
diff --git a/test/benchmarks/tests/readStream_x10000_levelup.js b/test/benchmarks/tests/readStream_x10000_levelup.js
new file mode 100644
index 0000000..d626d3c
--- /dev/null
+++ b/test/benchmarks/tests/readStream_x10000_levelup.js
@@ -0,0 +1,3 @@
+
+module.exports = require("./readStream_x100000_levelup").fn.bind(null)
+module.exports.setup = require("./readStream_x100000_levelup").setupFn.bind(null, 10000)
diff --git a/test/benchmarks/tests/readStream_x1000_levelup.js b/test/benchmarks/tests/readStream_x1000_levelup.js
new file mode 100644
index 0000000..87df599
--- /dev/null
+++ b/test/benchmarks/tests/readStream_x1000_levelup.js
@@ -0,0 +1,3 @@
+
+module.exports = require("./readStream_x100000_levelup").fn.bind(null)
+module.exports.setup = require("./readStream_x100000_levelup").setupFn.bind(null, 1000)
diff --git a/test/benchmarks/tests/readStream_x100_levelup.js b/test/benchmarks/tests/readStream_x100_levelup.js
new file mode 100644
index 0000000..4402559
--- /dev/null
+++ b/test/benchmarks/tests/readStream_x100_levelup.js
@@ -0,0 +1,3 @@
+
+module.exports = require("./readStream_x100000_levelup").fn.bind(null)
+module.exports.setup = require("./readStream_x100000_levelup").setupFn.bind(null, 100)
diff --git a/test/benchmarks/tests/readStream_x10_levelup.js b/test/benchmarks/tests/readStream_x10_levelup.js
new file mode 100644
index 0000000..c97aa3b
--- /dev/null
+++ b/test/benchmarks/tests/readStream_x10_levelup.js
@@ -0,0 +1,3 @@
+
+module.exports = require("./readStream_x100000_levelup").fn.bind(null)
+module.exports.setup = require("./readStream_x100000_levelup").setupFn.bind(null, 10)
diff --git a/test/binary-test.js b/test/binary-test.js
new file mode 100644
index 0000000..7420abb
--- /dev/null
+++ b/test/binary-test.js
@@ -0,0 +1,170 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Binary API', {
+    'setUp': function (done) {
+      common.commonSetUp.call(this, function () {
+        common.loadBinaryTestData(function (err, data) {
+          refute(err)
+          this.testData = data
+          done()
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'tearDown': common.commonTearDown
+
+  , 'sanity check on test data': function (done) {
+      assert(Buffer.isBuffer(this.testData))
+      common.checkBinaryTestData(this.testData, done)
+    }
+
+  , 'test put() and get() with binary value {encoding:binary}': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put('binarydata', this.testData, { encoding: 'binary' }, function (err) {
+          refute(err)
+          db.get('binarydata', { encoding: 'binary' }, function (err, value) {
+            refute(err)
+            assert(value)
+            common.checkBinaryTestData(value, done)
+          })
+        })
+      }.bind(this))
+    }
+
+  , 'test put() and get() with binary value {encoding:binary} on createDatabase()': function (done) {
+      this.openTestDatabase({ createIfMissing: true, errorIfExists: true, encoding: 'binary' }, function (db) {
+        db.put('binarydata', this.testData, function (err) {
+          refute(err)
+          db.get('binarydata', function (err, value) {
+            refute(err)
+            assert(value)
+            common.checkBinaryTestData(value, done)
+          })
+        })
+      }.bind(this))
+    }
+
+  , 'test put() and get() with binary key {encoding:binary}': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put(this.testData, 'binarydata', { encoding: 'binary' }, function (err) {
+          refute(err)
+          db.get(this.testData, { encoding: 'binary' }, function (err, value) {
+            refute(err)
+            assert(value instanceof Buffer, 'value is buffer')
+            assert.equals(value.toString(), 'binarydata')
+            done()
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test put() and get() with binary value {keyEncoding:utf8,valueEncoding:binary}': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put('binarydata', this.testData, { keyEncoding: 'utf8', valueEncoding: 'binary' }, function (err) {
+          refute(err)
+          db.get('binarydata', { keyEncoding: 'utf8', valueEncoding: 'binary' }, function (err, value) {
+            refute(err)
+            assert(value)
+            common.checkBinaryTestData(value, done)
+          })
+        })
+      }.bind(this))
+    }
+
+  , 'test put() and get() with binary value {keyEncoding:utf8,valueEncoding:binary} on createDatabase()': function (done) {
+      this.openTestDatabase({ createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'binary' }, function (db) {
+        db.put('binarydata', this.testData, function (err) {
+          refute(err)
+          db.get('binarydata', function (err, value) {
+            refute(err)
+            assert(value)
+            common.checkBinaryTestData(value, done)
+          })
+        })
+      }.bind(this))
+    }
+
+  , 'test put() and get() with binary key {keyEncoding:binary,valueEncoding:utf8}': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put(this.testData, 'binarydata', { keyEncoding: 'binary', valueEncoding: 'utf8' }, function (err) {
+          refute(err)
+          db.get(this.testData, { keyEncoding: 'binary', valueEncoding: 'utf8' }, function (err, value) {
+            refute(err)
+            assert.equals(value, 'binarydata')
+            done()
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test put() and get() with binary key & value {encoding:binary}': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put(this.testData, this.testData, { encoding: 'binary' }, function (err) {
+          refute(err)
+          db.get(this.testData, { encoding: 'binary' }, function (err, value) {
+            refute(err)
+            common.checkBinaryTestData(value, done)
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+
+  , 'test put() and del() and get() with binary key {encoding:binary}': function (done) {
+      this.openTestDatabase(function (db) {
+        db.put(this.testData, 'binarydata', { encoding: 'binary' }, function (err) {
+          refute(err)
+          db.del(this.testData, { encoding: 'binary' }, function (err) {
+            refute(err)
+            db.get(this.testData, { encoding: 'binary' }, function (err, value) {
+              assert(err)
+              refute(value)
+              done()
+            }.bind(this))
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'batch() with multiple puts': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(
+            [
+                { type: 'put', key: 'foo', value: this.testData }
+              , { type: 'put', key: 'bar', value: this.testData }
+              , { type: 'put', key: 'baz', value: 'abazvalue' }
+            ]
+          , { keyEncoding: 'utf8',valueEncoding: 'binary' }
+          , function (err) {
+              refute(err)
+              async.forEach(
+                  ['foo', 'bar', 'baz']
+                , function (key, callback) {
+                    db.get(key, { encoding: 'binary' }, function (err, value) {
+                      refute(err)
+                      if (key == 'baz') {
+                        assert(value instanceof Buffer, 'value is buffer')
+                        assert.equals(value.toString(), 'a' + key + 'value')
+                        callback()
+                      } else {
+                        common.checkBinaryTestData(value, callback)
+                      }
+                    })
+                  }
+                , done
+              )
+            }.bind(this)
+        )
+      }.bind(this))
+    }
+})
\ No newline at end of file
diff --git a/test/common.js b/test/common.js
new file mode 100644
index 0000000..f3a2d42
--- /dev/null
+++ b/test/common.js
@@ -0,0 +1,156 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var referee = require('referee')
+  , assert  = referee.assert
+  , refute  = referee.refute
+  , crypto  = require('crypto')
+  , async   = require('async')
+  , rimraf  = require('rimraf')
+  , fs      = require('fs')
+  , path    = require('path')
+  , delayed = require('delayed').delayed
+  , levelup = require('../lib/levelup.js')
+  , dbidx   = 0
+
+referee.add('isInstanceOf', {
+    assert: function (actual, expected) {
+        return actual instanceof expected
+    }
+  , refute: function (actual, expected) {
+        return !(actual instanceof expected)
+    }
+  , assertMessage: '${0} expected to be instance of ${1}'
+  , refuteMessage: '${0} expected not to be instance of ${1}'
+})
+
+referee.add('isUndefined', {
+    assert: function (actual) {
+        return actual === undefined
+    }
+  , refute: function (actual) {
+        return actual !== undefined
+    }
+  , assertMessage: '${0} expected to be undefined'
+  , refuteMessage: '${0} expected not to be undefined'
+})
+
+module.exports.nextLocation = function () {
+  return path.join(__dirname, '_levelup_test_db_' + dbidx++)
+}
+
+module.exports.cleanup = function (callback) {
+  fs.readdir(__dirname, function (err, list) {
+    if (err) return callback(err)
+
+    list = list.filter(function (f) {
+      return (/^_levelup_test_db_/).test(f)
+    })
+
+    if (!list.length)
+      return callback()
+
+    var ret = 0
+
+    list.forEach(function (f) {
+      rimraf(path.join(__dirname, f), function () {
+        if (++ret == list.length)
+          callback()
+      })
+    })
+  })
+}
+
+module.exports.openTestDatabase = function () {
+  var options = typeof arguments[0] == 'object' ? arguments[0] : { createIfMissing: true, errorIfExists: true }
+    , callback = typeof arguments[0] == 'function' ? arguments[0] : arguments[1]
+    , location = typeof arguments[0] == 'string' ? arguments[0] : module.exports.nextLocation()
+
+  rimraf(location, function (err) {
+    refute(err)
+    this.cleanupDirs.push(location)
+    levelup(location, options, function (err, db) {
+      refute(err)
+      if (!err) {
+        this.closeableDatabases.push(db)
+        callback(db)
+      }
+    }.bind(this))
+  }.bind(this))
+}
+
+module.exports.commonTearDown = function (done) {
+  async.forEach(
+      this.closeableDatabases
+    , function (db, callback) {
+        db.close(callback)
+      }
+    , module.exports.cleanup.bind(null, done)
+  )
+}
+
+module.exports.loadBinaryTestData = function (callback) {
+  fs.readFile(path.join(__dirname, 'data/testdata.bin'), callback)
+}
+
+module.exports.binaryTestDataMD5Sum = '920725ef1a3b32af40ccd0b78f4a62fd'
+
+module.exports.checkBinaryTestData = function (testData, callback) {
+  var md5sum = crypto.createHash('md5');
+  md5sum.update(testData)
+  assert.equals(md5sum.digest('hex'), module.exports.binaryTestDataMD5Sum)
+  callback()
+}
+
+module.exports.commonSetUp = function (done) {
+  this.cleanupDirs = []
+  this.closeableDatabases = []
+  this.openTestDatabase = module.exports.openTestDatabase.bind(this)
+  this.timeout = 10000
+  module.exports.cleanup(done)
+}
+
+module.exports.readStreamSetUp = function (done) {
+  module.exports.commonSetUp.call(this, function () {
+    var i, k
+
+    this.dataSpy    = this.spy()
+    this.endSpy     = this.spy()
+    this.sourceData = []
+
+    for (i = 0; i < 100; i++) {
+      k = (i < 10 ? '0' : '') + i
+      this.sourceData.push({
+          type  : 'put'
+        , key   : k
+        , value : Math.random()
+      })
+    }
+
+    this.verify = delayed(function (rs, done, data) {
+      if (!data) data = this.sourceData // can pass alternative data array for verification
+      assert.equals(this.endSpy.callCount, 1, 'ReadStream emitted single "end" event')
+      assert.equals(this.dataSpy.callCount, data.length, 'ReadStream emitted correct number of "data" events')
+      data.forEach(function (d, i) {
+        var call = this.dataSpy.getCall(i)
+        if (call) {
+          assert.equals(call.args.length, 1, 'ReadStream "data" event #' + i + ' fired with 1 argument')
+          refute.isNull(call.args[0].key, 'ReadStream "data" event #' + i + ' argument has "key" property')
+          refute.isNull(call.args[0].value, 'ReadStream "data" event #' + i + ' argument has "value" property')
+          assert.equals(call.args[0].key, d.key, 'ReadStream "data" event #' + i + ' argument has correct "key"')
+          assert.equals(
+              +call.args[0].value
+            , +d.value
+            , 'ReadStream "data" event #' + i + ' argument has correct "value"'
+          )
+        }
+      }.bind(this))
+      done()
+    }, 0.05, this)
+
+    done()
+
+  }.bind(this))
+}
diff --git a/test/compression-test.js b/test/compression-test.js
new file mode 100644
index 0000000..4d17f4f
--- /dev/null
+++ b/test/compression-test.js
@@ -0,0 +1,90 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var async      = require('async')
+  , du         = require('du')
+  , delayed    = require('delayed')
+  , levelup    = require('../')
+  , common     = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+  , compressableData = new Buffer(Array.apply(null, Array(1024 * 100)).map(function () { return 'aaaaaaaaaa' }).join(''))
+  , multiples = 10
+  , dataSize = compressableData.length * multiples
+
+  , verify = function (location, compression, done) {
+      du(location, function (err, size) {
+        if (err) return refute(err)
+        //console.log(Math.round((size / dataSize) * 100) + '% compression ratio (', size, 'b vs', dataSize, 'b)')
+        if (compression)
+          assert(size < dataSize, 'on-disk size (' + size + ') is less than data size (' + dataSize + ')')
+        else
+          assert(size >= dataSize, 'on-disk size (' + size + ') is greater than data size (' + dataSize + ')')
+        done()
+      })
+    }
+
+    // close, open, close again.. 'compaction' is also performed on open()s
+  , cycle = function (db, compression, callback) {
+      var location = db.location
+      db.close(function (err) {
+        if (err) return refute(err)
+        levelup(location, { errorIfExists: false, compression: compression }, function (err, db) {
+          if (err) return refute(err)
+          db.close(function (err) {
+            if (err) return refute(err)
+            callback()
+          })
+        })
+      })
+    }
+
+buster.testCase('Compression', {
+    'setUp': common.readStreamSetUp
+
+  , 'tearDown': common.commonTearDown
+
+  , 'test data is compressed by default (db.put())': function (done) {
+      this.openTestDatabase(function (db) {
+        async.forEach(
+            Array.apply(null, Array(multiples)).map(function (e, i) {
+              return [ i, compressableData ]
+            })
+          , function (args, callback) {
+              db.put.apply(db, args.concat([callback]))
+            }
+          , cycle.bind(null, db, true, delayed.delayed(verify.bind(null, db.location, true, done), 0.01))
+        )
+      })
+    }
+
+  , 'test data is not compressed with compression=false on open() (db.put())': function (done) {
+      this.openTestDatabase({ createIfMissing: true, errorIfExists: true, compression: false }, function (db) {
+        async.forEach(
+            Array.apply(null, Array(multiples)).map(function (e, i) {
+              return [ i, compressableData ]
+            })
+          , function (args, callback) {
+              db.put.apply(db, args.concat([callback]))
+            }
+          , cycle.bind(null, db, false, delayed.delayed(verify.bind(null, db.location, false, done), 0.01))
+        )
+      })
+    }
+
+  , 'test data is compressed by default (db.batch())': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(
+            Array.apply(null, Array(multiples)).map(function (e, i) {
+              return { type: 'put', key: i, value: compressableData }
+            })
+          , cycle.bind(null, db, false, delayed.delayed(verify.bind(null, db.location, false, done), 0.01))
+        )
+      })
+    }
+})
\ No newline at end of file
diff --git a/test/copy-test.js b/test/copy-test.js
new file mode 100644
index 0000000..5a8cfc0
--- /dev/null
+++ b/test/copy-test.js
@@ -0,0 +1,68 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Copy', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'copy full database': function (done) {
+      var sourceData = []
+
+      for (var i = 0; i < 100; i++) {
+        sourceData.push({
+            type  : 'put'
+          , key   : i
+          , value : Math.random()
+        })
+      }
+
+      var opensrc = function (callback) {
+            this.openTestDatabase(function (db) {
+              db.batch(sourceData.slice(), function (err) {
+                callback(err, db)
+              })
+            })
+          }.bind(this)
+
+        , opendst = function (callback) {
+            this.openTestDatabase(function (db) {
+              callback(null, db)
+            })
+          }.bind(this)
+
+        , verify = function (dstdb) {
+            async.forEach(
+                sourceData
+              , function (data, callback) {
+                  dstdb.get(data.key, function (err, value) {
+                    refute(err)
+                    assert.equals(+value.toString(), data.value, 'Destination data #' + data.key + ' has correct value')
+                    callback()
+                  })
+                }
+              , done
+            )
+          }.bind(this)
+
+      async.parallel(
+          { src: opensrc, dst: opendst }
+        , function (err, dbs) {
+            refute(err)
+            levelup.copy(dbs.src, dbs.dst, function (err) {
+              refute(err)
+              verify(dbs.dst)
+            })
+          }
+      )
+    }
+})
\ No newline at end of file
diff --git a/test/create-stream-vs-put-racecondition.js b/test/create-stream-vs-put-racecondition.js
new file mode 100644
index 0000000..9df5d71
--- /dev/null
+++ b/test/create-stream-vs-put-racecondition.js
@@ -0,0 +1,102 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup    = require('../lib/levelup.js')
+  , common     = require('./common')
+  , rimraf     = require('rimraf')
+  , async      = require('async')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+function makeTest (db, delay, done) {
+  // this should be an empty stream
+  var i = 0, j = 0, k = 0, m = 0
+  var streamEnd = false, putEnd = false
+
+  db.createReadStream()
+    .on('data', function (data) {
+      i++
+    })
+    .on('end', function () {
+      //since the readStream is created before inserting anything
+      //it should be empty? right?
+      assert.equals(i, 0, 'stream read the future')
+
+      if(putEnd) done()
+      streamEnd = true
+    })
+
+  db.on('put', function (key, value) {
+    j++
+  })
+
+
+  //insert 10 things, 
+  //then check the right number of events where emitted.
+  function insert() {
+    m ++ 
+    db.put('hello'+ k++/10, k, next)
+  }
+
+  delay(function () {
+    insert();insert();insert();insert();insert();
+    insert();insert();insert();insert();insert();
+  })
+
+  function next() {
+    if(--m) return
+      process.nextTick(function () {
+      assert.equals(j, 10)
+      assert.equals(i, 0)
+
+      if(streamEnd) done()
+      putEnd = true
+    })
+  }
+
+}
+
+buster.testCase('ReadStream', {
+    'setUp': common.readStreamSetUp
+
+  , 'tearDown': common.commonTearDown
+
+  //TODO: test various encodings
+  , 'readStream and then put in nextTick': function (done) {
+      this.openTestDatabase(function (db) {
+        makeTest(db, process.nextTick, done)
+      }.bind(this))
+    }
+  , 'readStream and then put in nextTick, defered open': function (done) {
+      var location = common.nextLocation()
+        , db       = levelup(location)
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+
+      makeTest(db, process.nextTick, done)
+
+    }
+  , 'readStream and then put, defered open': function (done) {
+      var location = common.nextLocation()
+        , db       = levelup(location)
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+
+      makeTest(db, function (f) {f()}, done)
+    }
+  , 'readStream and then put': function (done) {
+      this.openTestDatabase(function (db) {
+        makeTest(db, function (f) {f()}, done)
+      }.bind(this))
+    }
+
+})
+
+
+
diff --git a/test/data/testdata.bin b/test/data/testdata.bin
new file mode 100644
index 0000000..59229b9
Binary files /dev/null and b/test/data/testdata.bin differ
diff --git a/test/deferred-open-test.js b/test/deferred-open-test.js
new file mode 100644
index 0000000..3d22028
--- /dev/null
+++ b/test/deferred-open-test.js
@@ -0,0 +1,192 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Deferred open()', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'put() and get() on pre-opened database': function (done) {
+      var location = common.nextLocation()
+      // 1) open database without callback, opens in worker thread
+        , db       = levelup(location, { createIfMissing: true, errorIfExists: true, encoding: 'utf8' })
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+      assert.isObject(db)
+      assert.equals(db.location, location)
+
+      async.parallel([
+      // 2) insert 3 values with put(), these should be deferred until the database is actually open
+          db.put.bind(db, 'k1', 'v1')
+        , db.put.bind(db, 'k2', 'v2')
+        , db.put.bind(db, 'k3', 'v3')
+      ], function () {
+      // 3) when the callbacks have returned, the database should be open and those values should be in
+      //    verify that the values are there
+        async.forEach(
+            [1,2,3]
+          , function (k, cb) {
+              db.get('k' + k, function (err, v) {
+                refute(err)
+                assert.equals(v, 'v' + k)
+                cb()
+              })
+            }
+            // sanity, this shouldn't exist
+          , function () {
+              db.get('k4', function (err) {
+                assert(err)
+                // DONE
+                done()
+              })
+            }
+        )
+      })
+
+      // we should still be in a state of limbo down here, not opened or closed, but 'new'
+      refute(db.isOpen())
+      refute(db.isClosed())
+    }
+
+  , 'batch() on pre-opened database': function (done) {
+      var location = common.nextLocation()
+      // 1) open database without callback, opens in worker thread
+        , db       = levelup(location, { createIfMissing: true, errorIfExists: true, encoding: 'utf8' })
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+      assert.isObject(db)
+      assert.equals(db.location, location)
+
+      // 2) insert 3 values with batch(), these should be deferred until the database is actually open
+      db.batch([
+          { type: 'put', key: 'k1', value: 'v1' }
+        , { type: 'put', key: 'k2', value: 'v2' }
+        , { type: 'put', key: 'k3', value: 'v3' }
+      ], function () {
+      // 3) when the callbacks have returned, the database should be open and those values should be in
+      //    verify that the values are there
+        async.forEach(
+            [1,2,3]
+          , function (k, cb) {
+              db.get('k' + k, function (err, v) {
+                refute(err)
+                assert.equals(v, 'v' + k)
+                cb()
+              })
+            }
+            // sanity, this shouldn't exist
+          , function () {
+              db.get('k4', function (err) {
+                assert(err)
+                // DONE
+                done()
+              })
+            }
+        )
+      })
+
+      // we should still be in a state of limbo down here, not opened or closed, but 'new'
+      refute(db.isOpen())
+      refute(db.isClosed())
+    }
+    
+  , 'chained batch() on pre-opened database': function (done) {
+      var location = common.nextLocation()
+      // 1) open database without callback, opens in worker thread
+        , db       = levelup(location, { createIfMissing: true, errorIfExists: true, encoding: 'utf8' })
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+      assert.isObject(db)
+      assert.equals(db.location, location)
+
+      // 2) insert 3 values with batch(), these should be deferred until the database is actually open
+      db.batch()
+      .put('k1', 'v1')
+      .put('k2', 'v2')
+      .put('k3', 'v3')
+      .write(function () {
+      // 3) when the callbacks have returned, the database should be open and those values should be in
+      //    verify that the values are there
+        async.forEach(
+            [1,2,3]
+          , function (k, cb) {
+              db.get('k' + k, function (err, v) {
+                refute(err)
+                assert.equals(v, 'v' + k)
+                cb()
+              })
+            }
+            // sanity, this shouldn't exist
+          , function () {
+              db.get('k4', function (err) {
+                assert(err)
+                // DONE
+                done()
+              })
+            }
+        )
+        
+      })
+
+      // we should still be in a state of limbo down here, not opened or closed, but 'new'
+      refute(db.isOpen())
+      refute(db.isClosed())
+    }
+
+  , 'test deferred ReadStream': {
+        'setUp': common.readStreamSetUp
+
+      , 'simple ReadStream': function (done) {
+          this.openTestDatabase(function (db) {
+            var location = db.location
+            db.batch(this.sourceData.slice(), function (err) {
+              refute(err)
+              db.close(function (err) {
+                refute(err, 'no error')
+                db = levelup(location, { createIfMissing: false, errorIfExists: false })
+                var rs = db.createReadStream()
+                rs.on('data' , this.dataSpy)
+                rs.on('end'  , this.endSpy)
+                rs.on('close', this.verify.bind(this, rs, done))
+              }.bind(this))
+            }.bind(this))
+          }.bind(this))
+        }
+    }
+
+  , 'maxListeners warning': function (done) {
+      var location   = common.nextLocation()
+      // 1) open database without callback, opens in worker thread
+        , db         = levelup(location, { createIfMissing: true, errorIfExists: true, encoding: 'utf8' })
+        , stderrMock = this.mock(console)
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+      stderrMock.expects('error').never()
+
+      // 2) provoke an EventEmitter maxListeners warning
+      var toPut = 11
+
+      for (var i = 0; i < toPut; i++) {
+        db.put('some', 'string', function (err) {
+          refute(err)
+
+          if (!--toPut) {
+            done()
+          }
+        })
+      }
+    }
+})
diff --git a/test/destroy-repair-test.js b/test/destroy-repair-test.js
new file mode 100644
index 0000000..57f0714
--- /dev/null
+++ b/test/destroy-repair-test.js
@@ -0,0 +1,81 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup   = require('../lib/levelup.js')
+  , leveldown = require('leveldown')
+
+  , assert    = require('referee').assert
+  , refute    = require('referee').refute
+  , buster    = require('bustermove')
+
+buster.testCase('Destroy & Repair', {
+    'destroy() is alias for leveldown.destroy()': function () {
+      var ldmock = this.mock(leveldown)
+        , expect = ldmock.expects('destroy').once()
+      levelup.destroy()
+      ldmock.verify()
+      assert.same(expect.getCall(0).args[0], undefined)
+    }
+
+  , 'repair() is alias for leveldown.repair()': function () {
+      var ldmock = this.mock(leveldown)
+        , expect = ldmock.expects('repair').once()
+      levelup.repair()
+      ldmock.verify()
+      assert.same(expect.getCall(0).args[0], undefined)
+    }
+
+  , 'destroy() passes on arguments': function () {
+      var ldmock = this.mock(leveldown)
+        , args   = [ 'location', function () { } ]
+        , expect = ldmock
+            .expects('destroy')
+            .once()
+            .withExactArgs(args[0], args[1])
+
+      levelup.destroy.apply(null, args)
+      ldmock.verify()
+    }
+
+  , 'repair() passes on arguments': function () {
+      var ldmock = this.mock(leveldown)
+        , args   = [ 'location', function () { } ]
+        , expect = ldmock
+            .expects('repair')
+            .once()
+            .withExactArgs(args[0], args[1])
+
+      levelup.repair.apply(null, args)
+      ldmock.verify()
+    }
+
+  , 'destroy() substitutes missing callback argument': function () {
+      var ldmock = this.mock(leveldown)
+        , args   = [ 'location' ]
+        , expect = ldmock
+            .expects('destroy')
+            .once()
+            .withArgs(args[0])
+
+      levelup.destroy.apply(null, args)
+      ldmock.verify()
+      assert.equals(2, expect.getCall(0).args.length)
+      assert.isFunction(expect.getCall(0).args[1])
+    }
+
+  , 'repair() substitutes missing callback argument': function () {
+      var ldmock = this.mock(leveldown)
+        , args   = [ 'location' ]
+        , expect = ldmock
+            .expects('repair')
+            .once()
+            .withArgs(args[0])
+
+      levelup.repair.apply(null, args)
+      ldmock.verify()
+      assert.equals(2, expect.getCall(0).args.length)
+      assert.isFunction(expect.getCall(0).args[1])
+    }
+})
\ No newline at end of file
diff --git a/test/encoding-test.js b/test/encoding-test.js
new file mode 100644
index 0000000..9d2d561
--- /dev/null
+++ b/test/encoding-test.js
@@ -0,0 +1,160 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , common  = require('./common')
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Encoding', {
+    'setUp': common.readStreamSetUp
+
+  , 'tearDown': common.commonTearDown
+
+  , 'test safe decode in get()': function (done) {
+      this.openTestDatabase(
+          { createIfMissing: true, errorIfExists: true, encoding: 'utf8' }
+        , function (db) {
+            db.put('foo', 'this {} is [] not : json', function (err) {
+              refute(err)
+              db.close(function (err) {
+                refute(err)
+                db = levelup(db.location, { createIfMissing: false, errorIfExists: false, valueEncoding: 'json' })
+                db.get('foo', function (err, value) {
+                  assert(err)
+                  assert.equals('EncodingError', err.name)
+                  refute(value)
+                  db.close(done)
+                })
+              })
+            })
+          }
+      )
+    }
+
+  , 'test safe decode in readStream()': function (done) {
+      this.openTestDatabase(
+          { createIfMissing: true, errorIfExists: true, encoding: 'utf8' }
+        , function (db) {
+            db.put('foo', 'this {} is [] not : json', function (err) {
+              refute(err)
+              db.close(function (err) {
+                refute(err)
+
+                var dataSpy  = this.spy()
+                  , errorSpy = this.spy()
+
+                db = levelup(db.location, { createIfMissing: false, errorIfExists: false, valueEncoding: 'json' })
+                db.readStream()
+                  .on('data', dataSpy)
+                  .on('error', errorSpy)
+                  .on('close', function () {
+                    assert.equals(dataSpy.callCount, 0, 'no data')
+                    assert.equals(errorSpy.callCount, 1, 'error emitted')
+                    assert.equals('EncodingError', errorSpy.getCall(0).args[0].name)
+                    db.close(done)
+                  })
+              }.bind(this))
+            }.bind(this))
+          }.bind(this)
+      )
+    }
+
+  , 'test encoding = valueEncoding': function (done) {
+      // write a value as JSON, read as utf8 and check
+      // the fact that we can get with keyEncoding of utf8 should demonstrate that
+      // the key is not encoded as JSON
+      this.openTestDatabase({ encoding: 'json' }, function (db) {
+        db.put('foo:foo', { bar: 'bar' }, function (err) {
+          refute(err)
+          db.get('foo:foo', { keyEncoding: 'utf8', valueEncoding: 'utf8' }, function (err, value) {
+            refute(err)
+            assert.equals(value, '{"bar":"bar"}')
+            db.close(done)
+          })
+        })
+      })
+    }
+  , 'test write-stream encoding': function (done) {
+      this.openTestDatabase({ encoding: 'json' }, function (db) {
+        var ws = db.createWriteStream({
+          keyEncoding : 'utf8',
+          valueEncoding : 'binary'
+        })
+        ws.on('close', function () {
+          db.get('foo', {
+            keyEncoding : 'utf8',
+            valueEncoding : 'binary'
+          }, function (err, val) {
+            refute(err)
+            assert.equals(val.toString(), '\u0001\u0002\u0003')
+            db.close(done)
+          })
+        })
+        ws.write({ key : 'foo', value : new Buffer([1, 2, 3]) })
+        ws.end()
+      })
+    }
+  , 'test write-stream chunk encoding': function (done) {
+      this.openTestDatabase({ encoding: 'json' }, function (db) {
+        var ws = db.createWriteStream({
+          keyEncoding : 'utf8',
+          valueEncoding : 'binary'
+        })
+        ws.on('close', function () {
+          db.get(new Buffer([1, 2, 3]), {
+            keyEncoding : 'binary',
+            valueEncoding : 'json'
+          }, function (err, val) {
+            refute(err)
+            assert.equals(val.some, 'json')
+            db.close(done)
+          })
+        })
+        ws.write({
+          key : new Buffer([1, 2, 3]),
+          value : { some : 'json' },
+          keyEncoding : 'binary',
+          valueEncoding : 'json'
+        })
+        ws.end()
+      })
+    }
+  , 'test batch op encoding': function (done) {
+      this.openTestDatabase({ encoding: 'json' }, function (db) {
+        db.batch([
+            {
+              type : 'put',
+              key : new Buffer([1, 2, 3]),
+              value : new Buffer([4, 5, 6]),
+              keyEncoding : 'binary',
+              valueEncoding : 'binary'
+            }
+          , {
+              type : 'put',
+              key : 'string',
+              value : 'string'
+            }
+        ], { keyEncoding : 'utf8', valueEncoding : 'utf8' },
+        function (err) {
+          refute(err)
+          db.get(new Buffer([1, 2, 3]), {
+            keyEncoding : 'binary',
+            valueEncoding : 'binary'
+          }, function (err, val) {
+            refute(err)
+            assert.equals(val.toString(), '\u0004\u0005\u0006')
+
+            db.get('string', { valueEncoding : 'utf8' }, function (err, val) {
+              refute(err)
+              assert.equals(val, 'string')
+              db.close(done)
+            })
+          })
+        })
+      })
+    }
+})
diff --git a/test/functional/binary-data-test.js b/test/functional/binary-data-test.js
new file mode 100644
index 0000000..f70b3f7
--- /dev/null
+++ b/test/functional/binary-data-test.js
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+/*
+ * This test unpacks a tar file, pushes that data into a
+ * database then compares the database data with the files
+ * on the filesystem.
+ * The different types of data are useful for testing, particularly
+ * the binary files.
+ */
+
+var async     = require('async')
+  , rimraf    = require('rimraf')
+  , tarcommon = require('./tarcommon')
+
+console.log('***************************************************')
+console.log('RUNNING BINARY-DATA-TEST...')
+
+async.series([
+    // pre-clean
+    rimraf.bind(null, tarcommon.dblocation)
+  , rimraf.bind(null, tarcommon.datadir)
+    // extract data for comparison
+  , tarcommon.extract.bind(null, tarcommon.datatar, tarcommon.datadir)
+    // open database
+  , tarcommon.opendb.bind(null, tarcommon.dblocation)
+    // push the data into a database
+  , tarcommon.fstreamWrite
+    // run a sync put & del to force an fs sync
+  , tarcommon.sync
+    // verify database entries are the same as the files
+  , tarcommon.verify
+    // clean up
+  , rimraf.bind(null, tarcommon.dblocation)
+  , rimraf.bind(null, tarcommon.datadir)
+], function (err) {
+  if (err) console.error('Error', err)
+  else console.log('No errors? All good then!')
+  console.log('***************************************************')
+  process.exit(err ? -1 : 0)
+})
\ No newline at end of file
diff --git a/test/functional/compat-test.js b/test/functional/compat-test.js
new file mode 100644
index 0000000..68a182c
--- /dev/null
+++ b/test/functional/compat-test.js
@@ -0,0 +1,50 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+/*
+ * This test verifies that an existing database contains the
+ * correct data, by comparing it to the original data contained
+ * in a tar file.
+ * Useful for comparing across LevelDB versions.
+ */
+
+var async      = require('async')
+  , rimraf     = require('rimraf')
+  , path       = require('path')
+  , tarcommon  = require('./tarcommon')
+
+  , dbtar      = path.join(__dirname, 'test-data.db.tar')
+  , dblocation = path.join(__dirname, 'levelup_test_compat.db')
+
+function runTest (dbtar, callback) {
+  async.series([
+      // pre-clean
+      rimraf.bind(null, tarcommon.dblocation)
+    , rimraf.bind(null, dblocation)
+    , rimraf.bind(null, tarcommon.datadir)
+      // extract existing database
+    , tarcommon.extract.bind(null, dbtar, __dirname)
+      // extract data for comparison
+    , tarcommon.extract.bind(null, tarcommon.datatar, tarcommon.datadir)
+      // open database
+    , tarcommon.opendb.bind(null, dblocation)
+      // verify database entries are the same as the files
+    , tarcommon.verify
+      // clean up
+    , rimraf.bind(null, tarcommon.dblocation)
+    , rimraf.bind(null, dblocation)
+    , rimraf.bind(null, tarcommon.datadir)
+  ], callback)
+}
+
+console.log('***************************************************')
+console.log('RUNNING COMPAT-DATA-TEST...')
+
+runTest(dbtar, function (err) {
+  if (err) throw err
+  console.log('No errors? All good then!')
+  console.log('***************************************************')
+  process.exit(err ? -1 : 0)
+})
\ No newline at end of file
diff --git a/test/functional/fstream-test.js b/test/functional/fstream-test.js
new file mode 100644
index 0000000..e02dec1
--- /dev/null
+++ b/test/functional/fstream-test.js
@@ -0,0 +1,102 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var assert       = require('referee').assert
+  , refute       = require('referee').refute
+  , fstream      = require('fstream')
+  , async        = require('async')
+  , mkfiletree   = require('mkfiletree')
+  , readfiletree = require('readfiletree')
+  , rimraf       = require('rimraf')
+  , bogan        = require('boganipsum')
+  , levelup      = require('../../lib/levelup')
+
+  , fixtureFiles = {
+        'foo': 'FOO!\n'
+      , 'a directory': {
+            'bogantastic.txt': bogan()
+          , 'subdir': {
+                'boganmeup.dat': bogan()
+              , 'sub sub dir': {
+                    'bar': 'BAR!\n'
+                  , 'maaaaaaaate': bogan()
+                }
+              , 'bang': 'POW'
+            }
+          , 'boo': 'W00t'
+        }
+    }
+  , dblocation = 'levelup_test_fstream.db'
+
+  , opendb = function (dir, callback) {
+      levelup(dblocation, { createIfMissing: true , errorIfExists: false }, function (err, db) {
+        refute(err)
+        callback(null, dir, db)
+      })
+    }
+
+  , fstreamWrite = function (dir, db, callback) {
+      fstream.Reader(dir)
+        .pipe(db.writeStream({ fstreamRoot: dir })
+          .on('close', function () {
+            db.close(function (err) {
+              refute(err)
+              callback(null, dir)
+            })
+          }))
+    }
+
+  , fstreamRead = function (dir, db, callback) {
+      db.readStream({ type: 'fstream' })
+        .pipe(new fstream.Writer({ path: dir + '.out', type: 'Directory' })
+          .on('close', function () {
+            db.close(function (err) {
+              refute(err)
+              callback(null, dir)
+            })
+          })
+        )
+    }
+
+  , verify = function (dir, obj, callback) {
+      assert.equals(obj, fixtureFiles)
+      console.log('Guess what?? It worked!!')
+      callback(null, dir)
+    }
+
+  , cleanUp = function (dir, callback) {
+      async.parallel([
+          rimraf.bind(null, dir + '.out')
+        , rimraf.bind(null, dblocation)
+        , mkfiletree.cleanUp
+      ], callback)
+    }
+
+process.on('uncaughtException', function (err) {
+  refute(err)
+})
+
+console.log('***************************************************')
+console.log('RUNNING FSTREAM-TEST...')
+
+async.waterfall([
+    rimraf.bind(null, dblocation)
+  , mkfiletree.makeTemp.bind(null, 'levelup_test_fstream', fixtureFiles)
+  , opendb
+  , fstreamWrite
+  , opendb
+  , fstreamRead
+  , function (dir, callback) {
+      readfiletree(dir, function (err, obj) {
+        refute(err)
+        callback(err, dir, obj)
+      })
+    }
+  , verify
+  , cleanUp
+  , function () {
+      console.log('***************************************************')
+    }
+])
\ No newline at end of file
diff --git a/test/functional/tarcommon.js b/test/functional/tarcommon.js
new file mode 100644
index 0000000..41910da
--- /dev/null
+++ b/test/functional/tarcommon.js
@@ -0,0 +1,97 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var assert       = require('referee').assert
+  , fs           = require('fs')
+  , path         = require('path')
+  , fstream      = require('fstream')
+  , tar          = require('tar')
+  , crypto       = require('crypto')
+  , levelup      = require('../../lib/levelup')
+
+  , dblocation   = path.join(__dirname, 'levelup_test_binary.db')
+  , datatar      = path.join(__dirname, 'test-data.tar')
+  , datadir      = path.join(__dirname, 'test-data')
+  , db
+  , expectedEntries
+
+module.exports.dblocation = dblocation
+module.exports.datatar    = datatar
+module.exports.datadir    = datadir
+
+module.exports.opendb = function (dblocation, callback) {
+  levelup(
+      dblocation
+    , { createIfMissing: true , errorIfExists: false, keyEncoding: 'utf8', valueEncoding: 'binary' }
+    , function (err, _db) {
+        db = _db
+        console.log('Opened database...')
+        callback(err)
+      }
+  )
+}
+
+module.exports.extract = function (tarfile, dir, callback) {
+  expectedEntries = 0
+  fs.createReadStream(tarfile)
+    .pipe(tar.Extract({ path: dir }))
+    .on('entry', function (entry) {
+      if (entry.props.File || entry.File || entry.type == 'File')
+        expectedEntries++
+    })
+    .on('end', function () {
+      console.log('Extracted tar file...')
+      callback()
+    })
+}
+
+module.exports.fstreamWrite = function (callback) {
+  fstream.Reader(datadir)
+    .pipe(db.writeStream({ fstreamRoot: path.resolve(__dirname) })
+      .on('close', function () {
+        console.log('Piped data to database...')
+        callback()
+      }))
+      .on('error', callback)
+}
+
+// using sync:true will force a flush to the fs, otherwise the readStream() is too
+// quick and won't get the full data
+module.exports.sync = function (callback) {
+  db.put('__', '__', { sync: true }, function (err) {
+    if (err) return callback(err)
+    db.del('__', { sync: true }, callback)
+  })
+}
+
+module.exports.verify = function (callback) {
+  var entries = 0
+  db.readStream()
+    .on('data', function (data) {
+      var md5sum = crypto.createHash('md5')
+        , dbmd5sum
+
+      md5sum.update(data.value)
+      dbmd5sum = md5sum.digest('hex')
+      md5sum = crypto.createHash('md5')
+      entries++
+      fs.createReadStream(path.join(__dirname, data.key))
+        .on('data', function (d) { md5sum.update(d) })
+        .on('end', function () {
+          var fsmd5sum = md5sum.digest('hex')
+          assert.equals(
+              dbmd5sum
+            , fsmd5sum
+            , 'MD5 sum compare of ' + data.key + ' failed (' + dbmd5sum + ' != ' + fsmd5sum + ')'
+          )
+        })
+    })
+    .on('end', function () {
+      assert.equals(entries, expectedEntries, 'correct number of entries in the database')
+      console.log('Finished comparing database entries...')
+      console.log('Cleaning up...')
+      callback()
+    })
+}
\ No newline at end of file
diff --git a/test/functional/test-data.db.tar b/test/functional/test-data.db.tar
new file mode 100644
index 0000000..062f46d
Binary files /dev/null and b/test/functional/test-data.db.tar differ
diff --git a/test/functional/test-data.tar b/test/functional/test-data.tar
new file mode 100644
index 0000000..2a69b32
Binary files /dev/null and b/test/functional/test-data.tar differ
diff --git a/test/get-put-del-test.js b/test/get-put-del-test.js
new file mode 100644
index 0000000..e843081
--- /dev/null
+++ b/test/get-put-del-test.js
@@ -0,0 +1,185 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var errors  = require('../lib/errors.js')
+  , async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('get() / put() / del()', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'Simple operations': {
+        'get() on empty database causes error': function (done) {
+          this.openTestDatabase(function (db) {
+            db.get('undefkey', function (err, value) {
+              refute(value)
+              assert.isInstanceOf(err, Error)
+              assert.isInstanceOf(err, errors.LevelUPError)
+              assert.isInstanceOf(err, errors.NotFoundError)
+              assert(err.notFound === true, 'err.notFound is `true`')
+              assert.equals(err.status, 404, 'err.status is 404')
+              assert.match(err, '[undefkey]')
+              done()
+            })
+          })
+        }
+
+      , 'put() and get() simple string key/value pairs': function (done) {
+          this.openTestDatabase(function (db) {
+            db.put('some key', 'some value stored in the database', function (err) {
+              refute(err)
+              db.get('some key', function (err, value) {
+                refute(err)
+                assert.equals(value, 'some value stored in the database')
+                done()
+              })
+            })
+          })
+        }
+
+      , 'del() on empty database doesn\'t cause error': function (done) {
+          this.openTestDatabase(function (db) {
+            db.del('undefkey', function (err) {
+              refute(err)
+              done()
+            })
+          })
+        }
+
+      , 'del() works on real entries': function (done) {
+          this.openTestDatabase(function (db) {
+            async.series(
+                [
+                    function (callback) {
+                      async.forEach(
+                          ['foo', 'bar', 'baz']
+                        , function (key, callback) {
+                            db.put(key, 1 + Math.random(), callback)
+                          }
+                        , callback
+                      )
+                    }
+                  , function (callback) {
+                      db.del('bar', callback)
+                    }
+                  , function (callback) {
+                      async.forEach(
+                          ['foo', 'bar', 'baz']
+                        , function (key, callback) {
+                            db.get(key, function (err, value) {
+                              // we should get foo & baz but not bar
+                              if (key == 'bar') {
+                                assert(err)
+                                refute(value)
+                              } else {
+                                refute(err)
+                                assert(value)
+                              }
+                              callback()
+                            })
+                          }
+                        , callback
+                      )
+                    }
+                ]
+              , done
+            )
+          })
+        }
+    }
+
+  , 'test get() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.get.bind(db)
+          , { name: 'ReadError', message: 'get() requires key and callback arguments' }
+          , 'no-arg get() throws'
+        )
+
+        assert.exception(
+            db.get.bind(db, 'foo')
+          , { name: 'ReadError', message: 'get() requires key and callback arguments' }
+          , 'callback-less, 1-arg get() throws'
+        )
+
+        assert.exception(
+            db.get.bind(db, 'foo', {})
+          , { name: 'ReadError', message: 'get() requires key and callback arguments' }
+          , 'callback-less, 2-arg get() throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test put() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.put.bind(db)
+          , { name: 'WriteError', message: 'put() requires key and value arguments' }
+          , 'no-arg put() throws'
+        )
+
+        assert.exception(
+            db.put.bind(db, 'foo')
+          , { name: 'WriteError', message: 'put() requires key and value arguments' }
+          , 'callback-less, 1-arg put() throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test del() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.del.bind(db)
+          , { name: 'WriteError', message: 'del() requires a key argument' }
+          , 'no-arg del() throws'
+        )
+
+        done()
+      })
+    }
+
+  , 'test approximateSize() throwables': function (done) {
+      this.openTestDatabase(function (db) {
+
+        assert.exception(
+            db.approximateSize.bind(db)
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'no-arg approximateSize() throws'
+        )
+
+        assert.exception(
+            db.approximateSize.bind(db, 'foo')
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'callback-less, 1-arg approximateSize() throws'
+        )
+
+        assert.exception(
+            db.approximateSize.bind(db, 'foo', 'bar')
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'callback-less, 2-arg approximateSize() throws'
+        )
+
+        assert.exception(
+            db.approximateSize.bind(db, 'foo', 'bar', {})
+          , { name: 'ReadError', message: 'approximateSize() requires start, end and callback arguments' }
+          , 'callback-less, 3-arg approximateSize(), no cb throws'
+        )
+
+        done()
+      })
+    }
+})
\ No newline at end of file
diff --git a/test/idempotent-test.js b/test/idempotent-test.js
new file mode 100644
index 0000000..24a146c
--- /dev/null
+++ b/test/idempotent-test.js
@@ -0,0 +1,57 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Idempotent open & close', {
+    'setUp': common.readStreamSetUp
+
+  , 'tearDown': common.commonTearDown
+
+  , 'call open twice, should emit "open" once': function (done) {
+      var location = common.nextLocation()
+        , n = 0
+        , m = 0
+        , db
+        , close = function () {
+            var closing = this.spy()
+            db.on('closing', closing)
+            db.on('closed', function () {
+              assert.equals(closing.callCount, 1)
+              assert.equals(closing.getCall(0).args, [])
+              done()
+            })
+
+            //close needs to be idempotent too.
+            db.close()
+            process.nextTick(db.close.bind(db))
+          }.bind(this)
+
+      this.cleanupDirs.push(location)
+
+      db = levelup(
+          location
+        , { createIfMissing: true }
+        , function () {
+            assert.equals(n++, 0, 'callback should fire only once')
+            if (n && m)
+              close()
+          }
+      )
+
+      db.on('open', function () {
+        assert.equals(m++, 0, 'callback should fire only once')
+        if (n && m)
+          close()
+      })
+
+      db.open()
+    }
+})
diff --git a/test/init-test.js b/test/init-test.js
new file mode 100644
index 0000000..a368d35
--- /dev/null
+++ b/test/init-test.js
@@ -0,0 +1,217 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , errors  = require('../lib/errors.js')
+  , fs      = require('fs')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+  , MemDOWN = require('memdown')
+
+buster.testCase('Init & open()', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'levelup()': function () {
+      assert.isFunction(levelup)
+      assert.equals(levelup.length, 3) // location, options & callback arguments
+      assert.exception(levelup, 'InitializationError') // no location
+    }
+
+  , 'default options': function (done) {
+      var location = common.nextLocation()
+      levelup(location, { createIfMissing: true, errorIfExists: true }, function (err, db) {
+        refute(err, 'no error')
+        assert.isTrue(db.isOpen())
+        this.closeableDatabases.push(db)
+        this.cleanupDirs.push(location)
+        db.close(function (err) {
+          refute(err)
+
+          assert.isFalse(db.isOpen())
+
+          levelup(location, function (err, db) { // no options object
+            refute(err)
+            assert.isObject(db)
+            assert.isTrue(db.options.createIfMissing)
+            assert.isFalse(db.options.errorIfExists)
+            assert.equals(db.options.keyEncoding, 'utf8')
+            assert.equals(db.options.valueEncoding, 'utf8')
+            assert.equals(db.location, location)
+
+            // read-only properties
+            db.location = 'foo'
+            assert.equals(db.location, location)
+
+            done()
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'basic options': function (done) {
+      var location = common.nextLocation()
+      levelup(
+          location
+        , { createIfMissing: true, errorIfExists: true, encoding: 'binary' }
+        , function (err, db) {
+            refute(err)
+
+            this.closeableDatabases.push(db)
+            this.cleanupDirs.push(location)
+            assert.isObject(db)
+            assert.isTrue(db.options.createIfMissing)
+            assert.isTrue(db.options.errorIfExists)
+            assert.equals(db.options.keyEncoding, 'utf8')
+            assert.equals(db.options.valueEncoding, 'binary')
+            assert.equals(db.location, location)
+
+
+            // read-only properties
+            db.location = 'bar'
+            assert.equals(db.location, location)
+
+            done()
+          }.bind(this)
+      )
+    }
+
+  , 'options with encoding': function (done) {
+      var location = common.nextLocation()
+      levelup(
+          location
+        , { createIfMissing: true, errorIfExists: true, keyEncoding: 'ascii', valueEncoding: 'json' }
+        , function (err, db) {
+            refute(err)
+
+            this.closeableDatabases.push(db)
+            this.cleanupDirs.push(location)
+            assert.isObject(db)
+            assert.isTrue(db.options.createIfMissing)
+            assert.isTrue(db.options.errorIfExists)
+            assert.equals(db.options.keyEncoding, 'ascii')
+            assert.equals(db.options.valueEncoding, 'json')
+            assert.equals(db.location, location)
+
+
+            // read-only properties
+            db.location = 'bar'
+            assert.equals(db.location, location)
+
+            done()
+          }.bind(this)
+      )
+    }
+
+  , 'without callback': function (done) {
+      var location = common.nextLocation()
+        , db = levelup(location, { createIfMissing: true, errorIfExists: true })
+
+      this.closeableDatabases.push(db)
+      this.cleanupDirs.push(location)
+      assert.isObject(db)
+      assert.isTrue(db.options.createIfMissing)
+      assert.isTrue(db.options.errorIfExists)
+      assert.equals(db.location, location)
+
+      db.on("ready", function () {
+        assert.isTrue(db.isOpen())
+        done()
+      })
+    }
+
+  , 'open() with !createIfMissing expects error': function (done) {
+      levelup(this.cleanupDirs[0] = common.nextLocation(), { createIfMissing: false }, function (err, db) {
+        assert(err)
+        refute(db)
+        assert.isInstanceOf(err, Error)
+        assert.isInstanceOf(err, errors.LevelUPError)
+        assert.isInstanceOf(err, errors.OpenError)
+        assert(err.notFound === undefined, 'err.notFound is `undefined`, should only be on NotFoundError')
+        done()
+      }.bind(this))
+    }
+
+  , 'open() with createIfMissing expects directory to be created': function (done) {
+      levelup(this.cleanupDirs[0] = common.nextLocation(), { createIfMissing: true }, function (err, db) {
+        this.closeableDatabases.push(db)
+        refute(err)
+        assert.isTrue(db.isOpen())
+        fs.stat(this.cleanupDirs[0], function (err, stat) {
+          refute(err)
+          assert(stat.isDirectory())
+          done()
+        })
+      }.bind(this))
+    }
+
+  , 'open() with errorIfExists expects error if exists': function (done) {
+      levelup(this.cleanupDirs[0] = common.nextLocation(), { createIfMissing: true }, function (err, db) {
+        this.closeableDatabases.push(db)
+        refute(err) // sanity
+        levelup(this.cleanupDirs[0], { errorIfExists   : true }, function (err) {
+          assert(err)
+          assert.isInstanceOf(err, Error)
+          assert.isInstanceOf(err, errors.LevelUPError)
+          assert.isInstanceOf(err, errors.OpenError)
+          done()
+        })
+      }.bind(this))
+    }
+
+  , 'open() with !errorIfExists does not expect error if exists': function (done) {
+      levelup(this.cleanupDirs[0] = common.nextLocation(), { createIfMissing: true }, function (err, db) {
+        refute(err) // sanity
+        this.closeableDatabases.push(db)
+        assert.isTrue(db.isOpen())
+
+        db.close(function () {
+          assert.isFalse(db.isOpen())
+
+          levelup(this.cleanupDirs[0], { errorIfExists   : false }, function (err, db) {
+            refute(err)
+            this.closeableDatabases.push(db)
+            assert.isTrue(db.isOpen())
+            done()
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'constructor with options argument uses factory': function (done) {
+      var db = levelup({ db: MemDOWN })
+      assert.isNull(db.location, 'location property is null')
+      db.on('open', function () {
+        assert(db.db instanceof MemDOWN, 'using a memdown backend')
+        assert.same(db.db.location, '', 'db location property is ""')
+        db.put('foo', 'bar', function (err) {
+          refute(err, 'no error')
+          db.get('foo', function (err, value) {
+            assert.equals(value, 'bar', 'correct value')
+            done()
+          })
+        })
+      })
+    }
+
+  , 'constructor with only function argument uses factory': function (done) {
+      var db = levelup(MemDOWN)
+      assert.isNull(db.location, 'location property is null')
+      db.on('open', function () {
+        assert(db.db instanceof MemDOWN, 'using a memdown backend')
+        assert.same(db.db.location, '', 'db location property is ""')
+        db.put('foo', 'bar', function (err) {
+          refute(err, 'no error')
+          db.get('foo', function (err, value) {
+            assert.equals(value, 'bar', 'correct value')
+            done()
+          })
+        })
+      })
+    }
+})
diff --git a/test/inject-encoding-test.js b/test/inject-encoding-test.js
new file mode 100644
index 0000000..7a32725
--- /dev/null
+++ b/test/inject-encoding-test.js
@@ -0,0 +1,104 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , async   = require('async')
+  , common  = require('./common')
+  , msgpack = require('msgpack-js')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+  
+buster.testCase('JSON API', {
+    'setUp': function (done) {
+      common.commonSetUp.call(this, function () {
+        this.runTest = function (testData, assertType, done) {
+          var location = common.nextLocation()
+          this.cleanupDirs.push(location)
+          console.log(location)
+          levelup(location, {
+            createIfMissing: true,
+            errorIfExists: true,
+            encoding: {
+              encode: msgpack.encode,
+              decode: msgpack.decode,
+              buffer: true,
+              type: 'msgpack'
+            }
+          }, function (err, db) {
+            refute(err)
+            if (err) return
+
+            this.closeableDatabases.push(db)
+
+            async.parallel(
+                testData.map(function (d) { return db.put.bind(db, d.key, d.value) })
+              , function (err) {
+                  refute(err)
+
+                  async.forEach(
+                      testData
+                    , function (d, callback) {
+                        db.get(d.key, function (err, value) {
+                          if(err) console.error(err.stack)
+                          refute(err)
+                          assert[assertType](d.value, value)
+                          callback()
+                        })
+                      }
+                    , done
+                  )
+                }
+            )
+
+          }.bind(this))
+        }
+        done()
+      }.bind(this))
+    }
+
+  , 'tearDown': common.commonTearDown
+
+  , 'simple-object values in "json" encoding': function (done) {
+      this.runTest([
+            { key: '0', value: 0 }
+          , { key: '1', value: 1 }
+          , { key: 'string', value: 'a string' }
+          , { key: 'true', value: true }
+          , { key: 'false', value: false }
+        ], 'same', done)
+    }
+
+  , 'simple-object keys in "json" encoding': function (done) {
+      this.runTest([
+            { value: '0', key: 0 }
+          , { value: '1', key: 1 }
+          , { value: 'string', key: 'a string' }
+          , { value: 'true', key: true }
+          , { value: 'false', key: false }
+        ], 'same', done)
+    }
+
+  , 'complex-object values in "json" encoding': function (done) {
+      this.runTest([
+            { key: '0', value: {
+                foo: 'bar'
+              , bar: [ 1, 2, 3 ]
+              , bang: { yes: true, no: false }
+            }}
+        ], 'equals', done)
+    }
+
+  , 'complex-object keys in "json" encoding': function (done) {
+      this.runTest([
+            { value: '0', key: {
+                foo: 'bar'
+              , bar: [ 1, 2, 3 ]
+              , bang: { yes: true, no: false }
+            }}
+        ], 'same', done)
+    }
+});
diff --git a/test/json-test.js b/test/json-test.js
new file mode 100644
index 0000000..8f601bd
--- /dev/null
+++ b/test/json-test.js
@@ -0,0 +1,93 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('JSON API', {
+    'setUp': function (done) {
+      common.commonSetUp.call(this, function () {
+        this.runTest = function (testData, assertType, done) {
+          var location = common.nextLocation()
+          this.cleanupDirs.push(location)
+          levelup(location, { createIfMissing: true, errorIfExists: true, encoding: {encode: JSON.stringify, decode: JSON.parse }}, function (err, db) {
+            refute(err)
+            if (err) return
+
+            this.closeableDatabases.push(db)
+
+            async.parallel(
+                testData.map(function (d) { return db.put.bind(db, d.key, d.value) })
+              , function (err) {
+                  refute(err)
+
+                  async.forEach(
+                      testData
+                    , function (d, callback) {
+                        db.get(d.key, function (err, value) {
+                          if(err) console.error(err.stack)
+                          refute(err)
+                          assert[assertType](d.value, value)
+                          callback()
+                        })
+                      }
+                    , done
+                  )
+                }
+            )
+
+          }.bind(this))
+        }
+        done()
+      }.bind(this))
+    }
+
+  , 'tearDown': common.commonTearDown
+
+  , 'simple-object values in "json" encoding': function (done) {
+      this.runTest([
+            { key: '0', value: 0 }
+          , { key: '1', value: 1 }
+          , { key: 'string', value: 'a string' }
+          , { key: 'true', value: true }
+          , { key: 'false', value: false }
+        ], 'same', done)
+    }
+
+  , 'simple-object keys in "json" encoding': function (done) {
+      this.runTest([
+            { value: '0', key: 0 }
+          , { value: '1', key: 1 }
+          , { value: 'string', key: 'a string' }
+          , { value: 'true', key: true }
+          , { value: 'false', key: false }
+        ], 'same', done)
+    }
+
+  , 'complex-object values in "json" encoding': function (done) {
+      this.runTest([
+            { key: '0', value: {
+                foo: 'bar'
+              , bar: [ 1, 2, 3 ]
+              , bang: { yes: true, no: false }
+            }}
+        ], 'equals', done)
+    }
+
+  , 'complex-object keys in "json" encoding': function (done) {
+      this.runTest([
+            { value: '0', key: {
+                foo: 'bar'
+              , bar: [ 1, 2, 3 ]
+              , bang: { yes: true, no: false }
+            }}
+        ], 'same', done)
+    }
+});
diff --git a/test/key-value-streams-test.js b/test/key-value-streams-test.js
new file mode 100644
index 0000000..8536820
--- /dev/null
+++ b/test/key-value-streams-test.js
@@ -0,0 +1,109 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+  , delayed = require('delayed').delayed
+
+buster.testCase('Key and Value Streams', {
+    'setUp': function (done) {
+      common.commonSetUp.call(this, function () {
+        this.dataSpy    = this.spy()
+        this.endSpy     = this.spy()
+        this.sourceData = []
+
+        for (var i = 0; i < 100; i++) {
+          var k = (i < 10 ? '0' : '') + i
+          this.sourceData.push({
+              type  : 'put'
+            , key   : k
+            , value : Math.random()
+          })
+        }
+
+        this.sourceKeys = Object.keys(this.sourceData)
+          .map(function (k) { return this.sourceData[k].key }.bind(this))
+        this.sourceValues = Object.keys(this.sourceData)
+          .map(function (k) { return this.sourceData[k].value }.bind(this))
+
+        this.verify = delayed(function (rs, data, done) {
+          assert.equals(this.endSpy.callCount, 1, 'Stream emitted single "end" event')
+          assert.equals(this.dataSpy.callCount, data.length, 'Stream emitted correct number of "data" events')
+          data.forEach(function (d, i) {
+            var call = this.dataSpy.getCall(i)
+            if (call) {
+              //console.log('call', i, ':', call.args[0].key, '=', call.args[0].value, '(expected', d.key, '=', d.value, ')')
+              assert.equals(call.args.length, 1, 'Stream "data" event #' + i + ' fired with 1 argument')
+              assert.equals(+call.args[0].toString(), +d, 'Stream correct "data" event #' + i + ': ' + d)
+            }
+          }.bind(this))
+          done()
+        }, 0.05, this)
+
+        done()
+      }.bind(this))
+    }
+
+  , 'tearDown': common.commonTearDown
+
+  , 'test .keyStream()': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.keyStream()
+          rs.on('data', this.dataSpy)
+          rs.on('end', this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, this.sourceKeys, done))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test .readStream({keys:true,values:false})': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.readStream({ keys: true, values: false })
+          rs.on('data', this.dataSpy)
+          rs.on('end', this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, this.sourceKeys, done))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test .valueStream()': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.valueStream()
+          rs.on('data', this.dataSpy)
+          rs.on('end', this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, this.sourceValues, done))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test .readStream({keys:false,values:true})': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.readStream({ keys: false, values: true })
+          rs.on('data', this.dataSpy)
+          rs.on('end', this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, this.sourceValues, done))
+        }.bind(this))
+      }.bind(this))
+    }
+})
diff --git a/test/leveldown-substitution-test.js b/test/leveldown-substitution-test.js
new file mode 100644
index 0000000..542c18b
--- /dev/null
+++ b/test/leveldown-substitution-test.js
@@ -0,0 +1,56 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+  , MemDOWN = require('memdown')
+
+require('./common')
+
+buster.testCase('LevelDOWN Substitution', {
+    'test substitution of LevelDOWN with MemDOWN': function (done) {
+      var md       = new MemDOWN('foo')
+        , db       =
+            levelup('/somewhere/not/writable/booya!', { db: function () { return md } })
+        , entries  = []
+        , expected = [
+              { key: 'a', value: 'A' }
+            , { key: 'b', value: 'B' }
+            , { key: 'c', value: 'C' }
+            , { key: 'd', value: 'D' }
+            , { key: 'e', value: 'E' }
+            , { key: 'f', value: 'F' }
+            , { key: 'i', value: 'I' }
+          ]
+
+      db.put('f', 'F')
+      db.put('h', 'H')
+      db.put('i', 'I')
+      db.put('a', 'A')
+      db.put('c', 'C')
+      db.put('e', 'E')
+      db.del('g')
+      db.batch([
+          { type: 'put', key: 'd', value: 'D' }
+        , { type: 'del', key: 'h' }
+        , { type: 'put', key: 'b', value: 'B' }
+      ])
+
+      db.createReadStream()
+        .on('data', function (data) { entries.push(data) })
+        .on('error', function (err) { refute(err, 'readStream emitted an error') })
+        .on('close', function () {
+          assert.equals(entries, expected, 'correct entries')
+          assert.equals(
+              md._keys
+            , expected.map(function (e) { return e.key })
+            , 'memdown has the entries'
+          )
+          done()
+        })
+    }
+})
diff --git a/test/null-and-undefined-test.js b/test/null-and-undefined-test.js
new file mode 100644
index 0000000..8b91271
--- /dev/null
+++ b/test/null-and-undefined-test.js
@@ -0,0 +1,133 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , errors  = require('../lib/errors.js')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('null & undefined keys & values', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'null and undefined': {
+        'setUp': function (done) {
+          levelup(this.cleanupDirs[0] = common.nextLocation(), { createIfMissing: true }, function (err, db) {
+            refute(err) // sanity
+            this.closeableDatabases.push(db)
+            assert.isTrue(db.isOpen())
+            this.db = db
+            done()
+          }.bind(this))
+        }
+
+      , 'get() with null key causes error': function (done) {
+          this.db.get(null, function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'get() with undefined key causes error': function (done) {
+          this.db.get(undefined, function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'del() with null key causes error': function (done) {
+          this.db.del(null, function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'del() with undefined key causes error': function (done) {
+          this.db.del(undefined, function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'put() with null key causes error': function (done) {
+          this.db.put(null, 'foo', function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'put() with undefined key causes error': function (done) {
+          this.db.put(undefined, 'foo', function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'put() with null value causes error': function (done) {
+          this.db.put('foo', null, function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+
+      , 'put() with undefined value causes error': function (done) {
+          this.db.put('foo', undefined, function (err, value) {
+            refute(value)
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+      , 'batch() with undefined value causes error': function (done) {
+          this.db.batch([{key: 'foo', value: undefined, type: 'put'}]
+          , function (err) {
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+      , 'batch() with null value causes error': function (done) {
+          this.db.batch([{key: 'foo', value: null, type: 'put'}]
+          , function (err) {
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+      , 'batch() with undefined key causes error': function (done) {
+          this.db.batch([{key: undefined, value: 'bar', type: 'put'}]
+          , function (err) {
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+      , 'batch() with null key causes error': function (done) {
+          this.db.batch([{key: null, value: 'bar', type: 'put'}]
+          , function (err) {
+            assert.isInstanceOf(err, Error)
+            assert.isInstanceOf(err, errors.LevelUPError)
+            done()
+          })
+        }
+    }
+})
diff --git a/test/open-patchsafe-test.js b/test/open-patchsafe-test.js
new file mode 100644
index 0000000..c365eab
--- /dev/null
+++ b/test/open-patchsafe-test.js
@@ -0,0 +1,81 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup = require('../lib/levelup.js')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+function test(fun) {
+  return function (done) {
+    var location = common.nextLocation()
+    // 1) open database without callback, opens in worker thread
+      , db       = levelup(location, { createIfMissing: true, errorIfExists: true, encoding: 'utf8'})
+
+    this.closeableDatabases.push(db)
+    this.cleanupDirs.push(location)
+    assert.isObject(db)
+    assert.equals(db.location, location)
+
+    fun(db, done)
+    // we should still be in a state of limbo down here, not opened or closed, but 'new'
+    refute(db.isOpen())
+    refute(db.isClosed())
+  }
+}
+
+
+buster.testCase('Deferred open() is patch-safe', {
+    'setUp': common.commonSetUp
+  , 'tearDown': common.commonTearDown
+
+  , 'put() on pre-opened database': test(function (db, done) {
+      var put = db.put
+        , called = 0
+
+      db.put = function () {
+        called ++
+        return put.apply(this, arguments)
+      }
+
+      db.put('key', 'VALUE', function () {
+        assert.equals(called, 1)
+        done()
+      })
+    })
+  , 'del() on pre-opened database': test(function (db, done) {
+      var del = db.del
+        , called = 0
+
+      db.del = function () {
+        called ++
+        return del.apply(this, arguments)
+      }
+
+      db.del('key', function () {
+        assert.equals(called, 1)
+        done()
+      })
+    })
+  , 'batch() on pre-opened database': test(function (db, done) {
+      var batch = db.batch
+        , called = 0
+
+      db.batch = function () {
+        called ++
+        return batch.apply(this, arguments)
+      }
+
+      db.batch([
+        {key:'key', value: 'v', type: 'put'}
+      , {key:'key2', value: 'v2', type: 'put'}
+      ], function () {
+        assert.equals(called, 1)
+        done()
+      })
+    })
+})
\ No newline at end of file
diff --git a/test/optional-leveldown-test.js b/test/optional-leveldown-test.js
new file mode 100644
index 0000000..aa6964c
--- /dev/null
+++ b/test/optional-leveldown-test.js
@@ -0,0 +1,72 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+  , errors  = require('../lib/errors')
+
+function clearCache () {
+  delete require.cache[require.resolve('..')]
+  delete require.cache[require.resolve('leveldown')]
+  delete require.cache[require.resolve('leveldown/package')]
+  delete require.cache[require.resolve('../lib/util')]
+}
+
+buster.testCase('Optional LevelDOWN', {
+    'setUp': clearCache
+  , 'tearDown': clearCache
+
+  , 'test getLevelDOWN()': function () {
+      var util = require('../lib/util')
+      assert.same(util.getLevelDOWN(), require('leveldown'), 'correct leveldown provided')
+    }
+
+  , 'test wrong version': function () {
+      var levelup = require('..')
+      require('leveldown/package').version = '0.0.0'
+      assert.exception(levelup.bind(null, '/foo/bar'), function (err) {
+        if (err.name != 'LevelUPError')
+          return false
+        if (!/Installed version of LevelDOWN \(0\.0\.0\) does not match required version \(~\d+\.\d+\.\d+\)/.test(err.message))
+          return false
+        return true
+      })
+    }
+
+  , 'test no leveldown/package': function () {
+      var levelup = require('..')
+      // simulate an exception from a require() that doesn't resolved a package
+      Object.defineProperty(require.cache, require.resolve('leveldown/package'), {
+        get: function() {
+          throw new Error('Wow, this is kind of evil isn\'t it?')
+        }
+      })
+      assert.exception(levelup.bind(null, '/foo/bar'), function (err) {
+        if (err.name != 'LevelUPError')
+          return false
+        if ('Could not locate LevelDOWN, try `npm install leveldown`' != err.message)
+          return false
+        return true
+      })
+    }
+
+  , 'test no leveldown': function () {
+      var levelup = require('..')
+      // simulate an exception from a require() that doesn't resolved a package
+      Object.defineProperty(require.cache, require.resolve('leveldown'), {
+        get: function() {
+          throw new Error('Wow, this is kind of evil isn\'t it?')
+        }
+      })
+      assert.exception(levelup.bind(null, '/foo/bar'), function (err) {
+        if (err.name != 'LevelUPError')
+          return false
+        if ('Could not locate LevelDOWN, try `npm install leveldown`' != err.message)
+          return false
+        return true
+      })
+    }
+})
\ No newline at end of file
diff --git a/test/read-stream-test.js b/test/read-stream-test.js
new file mode 100644
index 0000000..8f6285d
--- /dev/null
+++ b/test/read-stream-test.js
@@ -0,0 +1,685 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var levelup    = require('../lib/levelup.js')
+  , common     = require('./common')
+  , SlowStream = require('slow-stream')
+  , delayed    = require('delayed')
+  , rimraf     = require('rimraf')
+  , async      = require('async')
+  , msgpack    = require('msgpack-js')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+  , bigBlob    = Array.apply(null, Array(1024 * 100)).map(function () { return 'aaaaaaaaaa' }).join('')
+
+buster.testCase('ReadStream', {
+    'setUp': common.readStreamSetUp
+
+  , 'tearDown': common.commonTearDown
+
+  //TODO: test various encodings
+
+  , 'test simple ReadStream': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test pausing': function (done) {
+      var calls = 0
+        , rs
+        , pauseVerify = function () {
+            assert.equals(calls, 5, 'stream should still be paused')
+            rs.resume()
+            pauseVerify.called = true
+          }
+        , onData = function () {
+            if (++calls == 5) {
+              rs.pause()
+              setTimeout(pauseVerify, 50)
+            }
+          }
+        , verify = function () {
+            assert.equals(calls, this.sourceData.length, 'onData was used in test')
+            assert(pauseVerify.called, 'pauseVerify was used in test')
+            this.verify(rs, done)
+          }.bind(this)
+
+      this.dataSpy = this.spy(onData) // so we can still verify
+
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          rs = db.createReadStream()
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('end', verify.bind(this))
+
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test destroy() immediately': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', function () {
+            assert.equals(this.dataSpy.callCount , 0, '"data" event was not fired')
+            assert.equals(this.endSpy.callCount  , 0, '"end" event was not fired')
+            done()
+          }.bind(this))
+          rs.destroy()
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test destroy() after close': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', function () {
+            rs.destroy()
+            done()
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test destroy() after closing db': function (done) {
+    this.openTestDatabase(function (db) {
+      db.batch(this.sourceData.slice(), function (err) {
+        refute(err)
+        db.close(function (err) {
+          var rs = db.createReadStream()
+          rs.destroy()
+          done()
+        }.bind(this))
+      }.bind(this))
+    }.bind(this))
+  }
+
+  , 'test destroy() twice': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+          rs.on('data' , function () {
+            rs.destroy()
+            rs.destroy()
+            done()
+          })
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test destroy() half way through': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+            , endSpy = this.spy()
+            , calls = 0
+          this.dataSpy = this.spy(function () {
+            if (++calls == 5)
+              rs.destroy()
+          })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , endSpy)
+          rs.on('close', function () {
+          //  assert.equals(this.readySpy.callCount, 1, 'ReadStream emitted single "ready" event')
+            // should do "data" 5 times ONLY
+            assert.equals(this.dataSpy.callCount, 5, 'ReadStream emitted correct number of "data" events (5)')
+            this.sourceData.slice(0, 5).forEach(function (d, i) {
+              var call = this.dataSpy.getCall(i)
+              assert(call)
+              if (call) {
+                assert.equals(call.args.length, 1, 'ReadStream "data" event #' + i + ' fired with 1 argument')
+                refute.isNull(call.args[0].key, 'ReadStream "data" event #' + i + ' argument has "key" property')
+                refute.isNull(call.args[0].value, 'ReadStream "data" event #' + i + ' argument has "value" property')
+                assert.equals(call.args[0].key, d.key, 'ReadStream "data" event #' + i + ' argument has correct "key"')
+                assert.equals(+call.args[0].value, +d.value, 'ReadStream "data" event #' + i + ' argument has correct "value"')
+              }
+            }.bind(this))
+            done()
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "reverse=true"': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ reverse: true })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData.reverse() // for verify
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "start"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: '50' })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // slice off the first 50 so verify() expects only the last 50 even though all 100 are in the db
+          this.sourceData = this.sourceData.slice(50)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "start" and "reverse=true"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: '50', reverse: true })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // reverse and slice off the first 50 so verify() expects only the first 50 even though all 100 are in the db
+          this.sourceData.reverse()
+          this.sourceData = this.sourceData.slice(49)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "start" being mid-way key (float)': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          // '49.5' doesn't actually exist but we expect it to start at '50' because '49' < '49.5' < '50' (in string terms as well as numeric)
+          var rs = db.createReadStream({ start: '49.5' })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // slice off the first 50 so verify() expects only the last 50 even though all 100 are in the db
+          this.sourceData = this.sourceData.slice(50)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "start" being mid-way key (float) and "reverse=true"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: '49.5', reverse: true })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // reverse & slice off the first 50 so verify() expects only the first 50 even though all 100 are in the db
+          this.sourceData.reverse()
+          this.sourceData = this.sourceData.slice(50)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "start" being mid-way key (string)': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          // '499999' doesn't actually exist but we expect it to start at '50' because '49' < '499999' < '50' (in string terms)
+          // the same as the previous test but we're relying solely on string ordering
+          var rs = db.createReadStream({ start: '499999' })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // slice off the first 50 so verify() expects only the last 50 even though all 100 are in the db
+          this.sourceData = this.sourceData.slice(50)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "end"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: '50' })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // slice off the last 49 so verify() expects only 0 -> 50 inclusive, even though all 100 are in the db
+          this.sourceData = this.sourceData.slice(0, 51)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "end" being mid-way key (float)': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: '50.5' })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // slice off the last 49 so verify() expects only 0 -> 50 inclusive, even though all 100 are in the db
+          this.sourceData = this.sourceData.slice(0, 51)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "end" being mid-way key (string)': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: '50555555' })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // slice off the last 49 so verify() expects only 0 -> 50 inclusive, even though all 100 are in the db
+          this.sourceData = this.sourceData.slice(0, 51)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "end" being mid-way key (float) and "reverse=true"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: '50.5', reverse: true })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData.reverse()
+          this.sourceData = this.sourceData.slice(0, 49)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with both "start" and "end"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: 30, end: 70 })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // should include 30 to 70, inclusive
+          this.sourceData = this.sourceData.slice(30, 71)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with both "start" and "end" and "reverse=true"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: 70, end: 30, reverse: true })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          // expect 70 -> 30 inclusive
+          this.sourceData.reverse()
+          this.sourceData = this.sourceData.slice(29, 70)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test json encoding': function (done) {
+      var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'json' }
+        , data = [
+              { type: 'put', key: 'aa', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ba', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ca', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+          ]
+
+      this.openTestDatabase(options, function (db) {
+        db.batch(data.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done, data))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test injectable encoding': function (done) {
+      var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: {
+          decode: msgpack.decode,
+          encode: msgpack.encode,
+          buffer: true
+        }}
+        , data = [
+              { type: 'put', key: 'aa', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ba', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ca', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+          ]
+
+      this.openTestDatabase(options, function (db) {
+        db.batch(data.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done, data))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() "reverse=true" not sticky (issue #6)': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+          // read in reverse, assume all's good
+          var rs = db.createReadStream({ reverse: true })
+          rs.on('close', function () {
+            // now try reading the other way
+            var rs = db.createReadStream()
+            rs.on('data' , this.dataSpy)
+            rs.on('end'  , this.endSpy)
+            rs.on('close', this.verify.bind(this, rs, done))
+          }.bind(this))
+          rs.resume()
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test ReadStream, start=0': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: 0 })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+        }.bind(this))
+      }.bind(this))
+    }
+
+    // we don't expect any data to come out of here because the keys start at '00' not 0
+    // we just want to ensure that we don't kill the process
+  , 'test ReadStream, end=0': function (done) {
+      this.openTestDatabase(function (db) {
+        // execute
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: 0 })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData = [ ]
+        }.bind(this))
+      }.bind(this))
+    }
+
+    // ok, so here's the deal, this is kind of obscure: when you have 2 databases open and
+    // have a readstream coming out from both of them with no references to the dbs left
+    // V8 will GC one of them and you'll get an failed assert from leveldb.
+    // This ISN'T a problem if you only have one of them open, even if the db gets GCed!
+    // Process:
+    //   * open
+    //   * batch write data
+    //   * close
+    //   * reopen
+    //   * create ReadStream, keeping no reference to the db
+    //   * pipe ReadStream through SlowStream just to make sure GC happens
+    //       - the error should occur here if the bug exists
+    //   * when both streams finish, verify all 'data' events happened
+  , 'test ReadStream without db ref doesn\'t get GCed': function (done) {
+      var dataSpy1   = this.spy()
+        , dataSpy2   = this.spy()
+        , location1  = common.nextLocation()
+        , location2  = common.nextLocation()
+        , sourceData = this.sourceData
+        , verify     = function () {
+            // no reference to `db` here, should have been GCed by now if it could be
+            assert(dataSpy1.callCount, sourceData.length)
+            assert(dataSpy2.callCount, sourceData.length)
+            async.parallel([ rimraf.bind(null, location1), rimraf.bind(null, location2) ], done)
+          }
+        , execute    = function (d, callback) {
+            // no reference to `db` here, could be GCed
+            d.readStream
+              .pipe(new SlowStream({ maxWriteInterval: 5 }))
+              .on('data', d.spy)
+              .on('close', delayed.delayed(callback, 0.05))
+          }
+        , open       = function (reopen, location, callback) {
+            levelup(location, { createIfMissing: !reopen, errorIfExists: !reopen }, callback)
+          }
+        , write      = function (db, callback) { db.batch(sourceData.slice(), callback) }
+        , close      = function (db, callback) { db.close(callback) }
+        , setup      = function (callback) {
+            async.map([ location1, location2 ], open.bind(null, false), function (err, dbs) {
+              refute(err)
+              if (err) return
+              async.map(dbs, write, function (err) {
+                refute(err)
+                if (err) return
+                async.forEach(dbs, close, callback)
+              })
+            })
+          }
+        , reopen    = function () {
+            async.map([ location1, location2 ], open.bind(null, true), function (err, dbs) {
+              refute(err)
+              if (err) return
+              async.forEach([
+                  { readStream: dbs[0].createReadStream(), spy: dataSpy1 }
+                , { readStream: dbs[1].createReadStream(), spy: dataSpy2 }
+              ], execute, verify)
+            })
+          }
+
+      setup(delayed.delayed(reopen, 0.05))
+    }
+
+
+    // this is just a fancy way of testing levelup('/path').createReadStream()
+    // i.e. not waiting for 'open' to complete
+    // the logic for this is inside the ReadStream constructor which waits for 'ready'
+  , 'test ReadStream on pre-opened db': function (done) {
+      var execute = function (db) {
+            // is in limbo
+            refute(db.isOpen())
+            refute(db.isClosed())
+
+            var rs = db.createReadStream()
+            rs.on('data' , this.dataSpy)
+            rs.on('end'  , this.endSpy)
+            rs.on('close', this.verify.bind(this, rs, done))
+          }.bind(this)
+        , setup = function (db) {
+            db.batch(this.sourceData.slice(), function (err) {
+              refute(err)
+              db.close(function (err) {
+                refute(err)
+                var db2 = levelup(db.location, { createIfMissing: false, errorIfExists: false, encoding: 'utf8' })
+                execute(db2)
+              })
+            }.bind(this))
+          }.bind(this)
+
+      this.openTestDatabase(setup)
+    }
+
+  , 'test readStream() with "limit"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ limit: 20 })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData = this.sourceData.slice(0, 20)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "start" and "limit"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ start: '20', limit: 20 })
+          //rs.on('ready', this.readySpy)
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData = this.sourceData.slice(20, 40)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "end" after "limit"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: '50', limit: 20 })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData = this.sourceData.slice(0, 20)
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readStream() with "end" before "limit"': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream({ end: '30', limit: 50 })
+          rs.on('data' , this.dataSpy)
+          rs.on('end'  , this.endSpy)
+          rs.on('close', this.verify.bind(this, rs, done))
+
+          this.sourceData = this.sourceData.slice(0, 31)
+        }.bind(this))
+      }.bind(this))
+    }
+
+    // can, fairly reliably, trigger a core dump if next/end isn't
+    // protected properly
+    // the use of large blobs means that next() takes time to return
+    // so we should be able to slip in an end() while it's working
+  , 'test iterator next/end race condition': function (done) {
+      var data = []
+        , i = 5
+        , v
+
+      while (i--) {
+        v = bigBlob + i
+        data.push({ type: 'put', key: v, value: v })
+      }
+
+      this.openTestDatabase(function (db) {
+        db.batch(data, function (err) {
+          refute(!!err)
+          var rs = db.createReadStream().on('close', done)
+          rs.once('data', rs.destroy.bind(rs))
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test can only end once': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          var rs = db.createReadStream()
+            .on('close', done)
+
+          process.nextTick(function () {
+            rs.destroy()
+          })
+
+        }.bind(this))
+      }.bind(this))
+    }
+
+  , 'test readable-stream at 1.0.x': function () {
+      // this is here to be an explicit reminder that we're tied to
+      // readable-stream at 1.0.x so if someone comes along and wants
+      // to bump version they'll have to come here and read that we're
+      // using Streams2 explicitly across Node versions and will
+      // probably delay Streams3 adoption until Node 0.12 is released
+      // as readable-stream at 1.1.x causes some problems with downstream
+      // modules
+      // see: https://github.com/rvagg/node-levelup/issues/216
+
+      assert(
+          (/^~1\.0\.\d+$/).test(require('../package.json').dependencies['readable-stream'])
+        , 'using readable-stream at 1.0.x'
+      )
+    }
+})
diff --git a/test/snapshot-test.js b/test/snapshot-test.js
new file mode 100644
index 0000000..4350e6c
--- /dev/null
+++ b/test/snapshot-test.js
@@ -0,0 +1,63 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var delayed    = require('delayed')
+  , common     = require('./common')
+  , SlowStream = require('slow-stream')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('Snapshots', {
+    'setUp': common.readStreamSetUp
+
+  , 'tearDown': common.commonTearDown
+
+  , 'test ReadStream implicit snapshot': function (done) {
+      this.openTestDatabase(function (db) {
+
+        // 1) Store 100 random numbers stored in the database
+        db.batch(this.sourceData.slice(), function (err) {
+          refute(err)
+
+          // 2) Create an iterator on the current data, pipe it through a SlowStream
+          //    to make *sure* that we're going to be reading it for longer than it
+          //    takes to overwrite the data in there.
+
+          var rs = db.readStream()
+          rs = rs.pipe(new SlowStream({ maxWriteInterval: 5 }))
+          rs.on('data' , this.dataSpy)
+          rs.once('end'  , this.endSpy)
+
+          rs.once('close', delayed.delayed(this.verify.bind(this, rs, done), 0.05))
+
+          process.nextTick(function () {
+            // 3) Concoct and write new random data over the top of existing items.
+            //    If we're not using a snapshot then then we'd expect the test
+            //    to fail because it'll pick up these new values rather than the
+            //    old ones.
+            var newData = []
+              , i
+              , k
+
+            for (i = 0; i < 100; i++) {
+              k = (i < 10 ? '0' : '') + i
+              newData.push({
+                  type  : 'put'
+                , key   : k
+                , value : Math.random()
+              })
+            }
+            // using sync:true here to ensure it's written fully to disk
+            db.batch(newData.slice(), { sync: true }, function (err) {
+              refute(err)
+              // we'll return here faster than it takes the readStream to complete
+            })
+          }.bind(this))
+        }.bind(this))
+      }.bind(this))
+    }
+})
diff --git a/test/test-10k-times.sh b/test/test-10k-times.sh
new file mode 100755
index 0000000..c68e67c
--- /dev/null
+++ b/test/test-10k-times.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+cd ..
+
+for (( i=1; i<=10000; i++ ))
+do
+    echo "Running test $i .."
+    npm test
+    if [ $? -ne 0 ]; then
+        break
+    fi
+done
diff --git a/test/write-stream-test.js b/test/write-stream-test.js
new file mode 100644
index 0000000..230e18b
--- /dev/null
+++ b/test/write-stream-test.js
@@ -0,0 +1,467 @@
+/* Copyright (c) 2012-2013 LevelUP contributors
+ * See list at <https://github.com/rvagg/node-levelup#contributing>
+ * MIT +no-false-attribs License <https://github.com/rvagg/node-levelup/blob/master/LICENSE>
+ */
+
+var async   = require('async')
+  , common  = require('./common')
+
+  , assert  = require('referee').assert
+  , refute  = require('referee').refute
+  , buster  = require('bustermove')
+
+buster.testCase('WriteStream', {
+    'setUp': function (done) {
+      common.commonSetUp.call(this, function () {
+        this.timeout = 1000
+
+        this.sourceData = []
+
+        for (var i = 0; i < 10; i++) {
+          this.sourceData.push({
+              type  : 'put'
+            , key   : i
+            , value : Math.random()
+          })
+        }
+
+        this.verify = function (ws, db, done, data) {
+          if (!data) data = this.sourceData // can pass alternative data array for verification
+          async.forEach(
+              data
+            , function (data, callback) {
+                db.get(data.key, function (err, value) {
+                  refute(err)
+                  assert.equals(+value, +data.value, 'WriteStream data #' + data.key + ' has correct value')
+                  callback()
+                })
+              }
+            , done
+          )
+        }
+
+        done()
+      }.bind(this))
+    }
+
+  , 'tearDown': common.commonTearDown
+
+  //TODO: test various encodings
+
+  , 'test simple WriteStream': function (done) {
+      this.openTestDatabase(function (db) {
+        var ws = db.createWriteStream()
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', this.verify.bind(this, ws, db, done))
+        this.sourceData.forEach(function (d) {
+          ws.write(d)
+        })
+        ws.once('ready', ws.end) // end after it's ready, nextTick makes this work OK
+      }.bind(this))
+    }
+
+  , 'test WriteStream with async writes': function (done) {
+      this.openTestDatabase(function (db) {
+        var ws = db.createWriteStream()
+
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', this.verify.bind(this, ws, db, done))
+        async.forEachSeries(
+            this.sourceData
+          , function (d, callback) {
+              // some should batch() and some should put()
+              if (d.key % 3) {
+                setTimeout(function () {
+                  ws.write(d)
+                  callback()
+                }, 10)
+              } else {
+                ws.write(d)
+                callback()
+              }
+            }
+          , function () {
+              ws.end()
+            }
+        )
+      }.bind(this))
+    }
+
+  /*
+    // exactly the same as previous but should avoid batch() writes
+  , 'test WriteStream with async writes and useBatch=false': function (done) {
+      this.openTestDatabase(function (db) {
+        db.batch = function () {
+          Array.prototype.slice.call(arguments).forEach(function (a) {
+            if (typeof a == 'function') a('Should not call batch()')
+          })
+        }
+
+        var ws = db.createWriteStream({ useBatch: false })
+
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', this.verify.bind(this, ws, db, done))
+        async.forEachSeries(
+            this.sourceData
+          , function (d, callback) {
+              if (d.key % 3) {
+                setTimeout(function () {
+                  ws.write(d)
+                  callback()
+                }, 10)
+              } else {
+                ws.write(d)
+                callback()
+              }
+            }
+          , function () {
+              ws.end()
+            }
+        )
+      }.bind(this))
+    }
+  */
+
+  , 'test end accepts data': function (done) {
+      this.openTestDatabase(function (db) {
+        var ws = db.createWriteStream()
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', this.verify.bind(this, ws, db, done))
+        var i = 0
+        this.sourceData.forEach(function (d) {
+          i ++
+          if (i < this.sourceData.length) {
+            ws.write(d)
+          } else {
+            ws.end(d)
+          }
+        }.bind(this))
+      }.bind(this))
+    }
+
+    // at the moment, destroySoon() is basically just end()
+  , 'test destroySoon()': function (done) {
+      this.openTestDatabase(function (db) {
+        var ws = db.createWriteStream()
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', this.verify.bind(this, ws, db, done))
+        this.sourceData.forEach(function (d) {
+          ws.write(d)
+        })
+        ws.once('ready', ws.destroySoon) // end after it's ready, nextTick makes this work OK
+      }.bind(this))
+    }
+
+  , 'test destroy()': function (done) {
+      var verify = function (ws, db) {
+        async.forEach(
+            this.sourceData
+          , function (data, callback) {
+              db.get(data.key, function (err, value) {
+                // none of them should exist
+                assert(err)
+                refute(value)
+                callback()
+              })
+            }
+          , done
+        )
+      }
+
+      this.openTestDatabase(function (db) {
+        var ws = db.createWriteStream()
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', verify.bind(this, ws, db))
+        this.sourceData.forEach(function (d) {
+          ws.write(d)
+        })
+        ws.once('ready', ws.destroy)
+      }.bind(this))
+    }
+
+  , 'test json encoding': function (done) {
+      var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'json' }
+        , data = [
+              { type: 'put', key: 'aa', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ba', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ca', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+          ]
+
+      this.openTestDatabase(options, function (db) {
+        var ws = db.createWriteStream()
+        ws.on('error', function (err) {
+          refute(err)
+        })
+        ws.on('close', this.verify.bind(this, ws, db, done, data))
+        data.forEach(function (d) {
+          ws.write(d)
+        })
+        ws.once('ready', ws.end) // end after it's ready, nextTick makes this work OK
+      }.bind(this))
+    }
+
+  , 'test del capabilities for each key/value': function (done) {
+
+      var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'json' }
+        , data = [
+              { type: 'put', key: 'aa', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ba', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { type: 'put', key: 'ca', value: { a: 'complex', obj: 100 } }
+            , { type: 'put', key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { type: 'put', key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+          ]
+
+      async.waterfall([
+        function (cb) {
+          this.openTestDatabase(options, function (db) {
+            cb(null, db);
+          });
+        }.bind(this),
+        function (db, cb) {
+          var ws = db.createWriteStream()
+          ws.on('error', function (err) {
+            refute(err)
+          })
+          ws.on('close', function () {
+            cb(null, db);
+          })
+          data.forEach(function (d) {
+            ws.write(d)
+          })
+
+          // end after it's ready, nextTick makes this work OK
+          ws.once('ready', ws.end)
+        },
+        function (db, cb) {
+          var delStream = db.createWriteStream()
+          delStream.on('error', function (err) {
+            refute(err)
+          })
+          delStream.on('close', function () {
+            cb(null, db);
+          })
+          data.forEach(function (d) {
+            d.type = 'del'
+            delStream.write(d)
+          })
+
+          // end after it's ready, nextTick makes this work OK
+          delStream.once('ready', delStream.end)
+        },
+        function (db, cb) {
+          async.forEach(
+              data
+            , function (data, callback) {
+                db.get(data.key, function (err, value) {
+                  // none of them should exist
+                  assert(err)
+                  refute(value)
+                  callback()
+                })
+              }
+            , cb
+          )
+        }
+      ], done)
+    }
+
+  , 'test del capabilities as constructor option': function (done) {
+
+      var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'json' }
+        , data = [
+              { key: 'aa', value: { a: 'complex', obj: 100 } }
+            , { key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { key: 'ba', value: { a: 'complex', obj: 100 } }
+            , { key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { key: 'ca', value: { a: 'complex', obj: 100 } }
+            , { key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+          ]
+
+      async.waterfall([
+        function (cb) {
+          this.openTestDatabase(options, function (db) {
+            cb(null, db);
+          });
+        }.bind(this),
+        function (db, cb) {
+          var ws = db.createWriteStream()
+          ws.on('error', function (err) {
+            refute(err)
+          })
+          ws.on('close', function () {
+            cb(null, db);
+          })
+          data.forEach(function (d) {
+            ws.write(d)
+          })
+
+          // end after it's ready, nextTick makes this work OK
+          ws.once('ready', ws.end)
+        },
+        function (db, cb) {
+          var delStream = db.createWriteStream({ type: 'del' })
+          delStream.on('error', function (err) {
+            refute(err)
+          })
+          delStream.on('close', function () {
+            cb(null, db);
+          })
+          data.forEach(function (d) {
+            delStream.write(d)
+          })
+
+          // end after it's ready, nextTick makes this work OK
+          delStream.once('ready', delStream.end)
+        },
+        function (db, cb) {
+          async.forEach(
+              data
+            , function (data, callback) {
+                db.get(data.key, function (err, value) {
+                  // none of them should exist
+                  assert(err)
+                  refute(value)
+                  callback()
+                })
+              }
+            , cb
+          )
+        }
+      ], done)
+    }
+
+  , 'test type at key/value level must take precedence on the constructor': function (done) {
+
+      var options = { createIfMissing: true, errorIfExists: true, keyEncoding: 'utf8', valueEncoding: 'json' }
+        , data = [
+              { key: 'aa', value: { a: 'complex', obj: 100 } }
+            , { key: 'ab', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { key: 'ac', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { key: 'ba', value: { a: 'complex', obj: 100 } }
+            , { key: 'bb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { key: 'bc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+            , { key: 'ca', value: { a: 'complex', obj: 100 } }
+            , { key: 'cb', value: { b: 'foo', bar: [ 1, 2, 3 ] } }
+            , { key: 'cc', value: { c: 'w00t', d: { e: [ 0, 10, 20, 30 ], f: 1, g: 'wow' } } }
+          ]
+        , exception = data[0]
+
+      exception['type'] = 'put'
+
+      async.waterfall([
+        function (cb) {
+          this.openTestDatabase(options, function (db) {
+            cb(null, db);
+          });
+        }.bind(this),
+        function (db, cb) {
+          var ws = db.createWriteStream()
+          ws.on('error', function (err) {
+            refute(err)
+          })
+          ws.on('close', function () {
+            cb(null, db);
+          })
+          data.forEach(function (d) {
+            ws.write(d)
+          })
+
+          // end after it's ready, nextTick makes this work OK
+          ws.once('ready', ws.end)
+        },
+        function (db, cb) {
+          var delStream = db.createWriteStream({ type: 'del' })
+          delStream.on('error', function (err) {
+            refute(err)
+          })
+          delStream.on('close', function () {
+            cb(null, db);
+          })
+          data.forEach(function (d) {
+            delStream.write(d)
+          })
+
+          // end after it's ready, nextTick makes this work OK
+          delStream.once('ready', delStream.end)
+        },
+        function (db, cb) {
+          async.forEach(
+              data
+            , function (data, callback) {
+                db.get(data.key, function (err, value) {
+                  if (data.type === 'put') {
+                    assert(value)
+                    callback()
+                  } else {
+                    assert(err)
+                    refute(value)
+                    callback()
+                  }
+                })
+              }
+            , cb
+          )
+        }
+      ], done)
+    }
+
+  , 'test ignoring pairs with the wrong type': function (done) {
+
+      async.waterfall([
+        function (cb) {
+          this.openTestDatabase(cb.bind(null, null))
+        }.bind(this),
+        function (db, cb) {
+          var ws = db.createWriteStream()
+          ws.on('error', function (err) {
+            refute(err)
+          })
+          ws.on('close', cb.bind(null, db))
+          this.sourceData.forEach(function (d) {
+            d.type = "x" + Math.random()
+            ws.write(d)
+          })
+          ws.once('ready', ws.end) // end after it's ready, nextTick makes this work OK
+        }.bind(this),
+        function (db, cb) {
+          async.forEach(
+              this.sourceData
+            , function (data, callback) {
+                db.get(data.key, function (err, value) {
+                  assert(err)
+                  refute(value)
+                  callback()
+                })
+              }
+            , cb
+          )
+
+        }.bind(this)
+      ], done)
+    }
+})

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-levelup.git



More information about the Pkg-javascript-commits mailing list