Repository: Jaymon/prom (language: python)

Issue:
Query read methods need to take kwargs
This doesn't work:

```python
with FooOrm.transaction() as conn:
     FooOrm.query.one(connection=conn)
```

Because `.one` doesn't take `kwargs` so you can't pass connection to it.

Implementing this will actually be harder than it looks since most of the read methods just create an iterator and they don't actually make the query until you start iterating or whatnot, so there might need to be some rethinking on how this is done, like creating the cursor and passing the cursor into the iterator or something like that.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index aeb4844..0aa0270 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,13 @@
 # Prom
 
-An opinionated lightweight orm for PostgreSQL or SQLite.
-
-Prom has been used in both single threaded and multi-threaded environments, including environments using Greenthreads.
+An opinionated asynchronous lightweight orm for PostgreSQL or SQLite.
 
 
 ## 1 Minute Getting Started with SQLite
 
 First, install prom:
 
-    $ pip install prom
+    $ pip install prom[sqlite]
 
 Set an environment variable:
 
@@ -35,7 +33,7 @@ Now go wild and create some `Foo` objects:
 
 ```python
 >>> for x in range(10):
-...     f = Foo.create(bar=x)
+...     f = await Foo.create(bar=x)
 ...
 >>>
 ```
@@ -43,13 +41,13 @@ Now go wild and create some `Foo` objects:
 Now query them:
 
 ```python
->>> f = Foo.query.one()
+>>> f = await Foo.query.one()
 >>> f.bar
 0
 >>> f.pk
 1
 >>>
->>> for f in Foo.query.in_bar([3, 4, 5]):
+>>> async for f in await Foo.query.in_bar([3, 4, 5]):
 ...     f.pk
 ...
 3
@@ -61,9 +59,9 @@ Now query them:
 Update them:
 
 ```python
->>> for f in Foo.query:
+>>> async for f in await Foo.query:
 ...     f.bar += 100
-...     f.save()
+...     await f.save()
 ...
 >>>
 ```
@@ -71,8 +69,8 @@ Update them:
 and get rid of them:
 
 ```python
->>> for f in Foo.query:
-...     f.delete()
+>>> async for f in await Foo.query:
+...     await f.delete()
 ...
 >>>
 ```
@@ -122,17 +120,14 @@ It's easy to have one set of `prom.Orm` children use one connection and another
 ```python
 import prom
 
-prom.configure("Interface://testuser:testpw@localhost/testdb#connection_1")
-prom.configure("Interface://testuser:testpw@localhost/testdb#connection_2")
-
 class Orm1(prom.Orm):
-    connection_name = "connection_1"
+    connection_name = "conn_1"
   
 class Orm2(prom.Orm):
-    connection_name = "connection_2"
+    connection_name = "conn_2"
 ```
 
-Now, any child class that extends `Orm1` will use `connection_1` and any child class that extends `Orm2` will use `connection_2`.
+Now, any child class that extends `Orm1` will use `conn_1` and any child class that extends `Orm2` will use `conn_2`.
 
 
 ## Creating Models
@@ -147,7 +142,7 @@ Checkout the [README](https://github.com/Jaymon/prom/blob/master/docs/README_QUE
 
 ## Versions
 
-While Prom will most likely work on other versions, Prom is tested to work on 2.7+ and 3.8.
+While Prom will most likely work on other versions, Prom is tested to work on 3.10.
 
 
 ## Installation
@@ -155,39 +150,18 @@ While Prom will most likely work on other versions, Prom is tested to work on 2.
 
 ### Postgres
 
-If you want to use Prom with Postgres, you need psycopg2:
+If you want to use Prom with Postgres:
 
     $ apt-get install libpq-dev python-dev
-    $ pip install psycopg
-
-
-### Green Threads
-
-If you want to use Prom with gevent, you'll need gevent and psycogreen:
-
-    $ pip install gevent
-    $ pip install psycogreen
-
-These are the versions we're using:
-
-    $ pip install "gevent==20.6.2"
-    $ pip install "psycogreen==1.0"
-
-Then you can setup Prom like this:
-
-```python
-from prom.interface.postgres import gevent
-gevent.patch_all()
-```
-
-Now you can use Prom in the same way you always have. If you would like to configure the threads and stuff, you can pass in some configuration options using the dsn, the three parameters are *async*, *pool_maxconn*, *pool_minconn*, and *pool_class*. The only one you'll really care about is *pool_maxconn* which sets how many connections should be created.
+    $ pip install prom[postgres]
 
 
 ### Prom
 
 Prom installs using pip:
 
-    $ pip install prom
+    $ pip install prom[sqlite]
+    $ pip install prom[postgres]
 
 and to install the latest and greatest:
 
@@ -202,5 +176,7 @@ Likewise, if you add a field (and the field is not required) then prom will go a
 
 If you want to install the tables manually, you can create a script or something and use the Orm's `install()` method:
 
-    SomeOrm.install()
+```python
+await SomeOrm.install()
+```
 
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..ed1fd37
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,36 @@
+version: "3.4"
+
+services:
+  database:
+    # https://hub.docker.com/_/postgres
+    image: postgres:15
+    restart: always
+    env_file:
+      - .env.local
+#     environment:
+#       - POSTGRES_DB=pgdb
+#       - POSTGRES_PASSWORD=pgpassword
+#       - POSTGRES_USER=postgres
+    volumes:
+      - ./cache/postgres/data:/var/lib/postgresql/data
+      - ./cache/postgres/initdb.d/extensions.sql:/docker-entrypoint-initdb.d/extensions.sql
+    ports:
+      - "5432:5432"
+    # https://stackoverflow.com/questions/57015003/log-all-queries-in-the-official-postgres-docker-image
+    command: [
+      "postgres",
+      # https://www.postgresql.org/docs/current/runtime-config-logging.html
+      "-c",
+      #"log_statement=all",
+      "log_statement=mod",
+#       "-c",
+#       "log_min_error_statement=WARNING",
+#       "-c",
+#       "log_min_messages=WARNING",
+#       "-c",
+#       "log_error_verbosity=TERSE",
+      "-c",
+      #"log_min_duration_statement=2000"
+      "log_min_duration_statement=0"
+    ]
+
diff --git a/docs/README_MODEL.md b/docs/README_MODEL.md
index 9313521..9764c03 100644
--- a/docs/README_MODEL.md
+++ b/docs/README_MODEL.md
@@ -21,6 +21,8 @@ The `field_type` is the python type you want the field to be. The supported fiel
 * `bytearray` - a blob.
 * `datetime` - a `datetime.datetime` value.
 * `date` - a `datetime.date` value.
+* `Enum` - a `enum.Enum` value.
+* `Orm` - a `prom.Orm` class, see the Foreign Keys section for details.
 
 The `field_required` is a boolean, it is true if the field needs to have a value, false if the field can be **NULL** in the db.
 
diff --git a/docs/README_QUERY.md b/docs/README_QUERY.md
index 5959aef..8c97be9 100644
--- a/docs/README_QUERY.md
+++ b/docs/README_QUERY.md
@@ -6,7 +6,7 @@ You can access the query, or table, instance for each `prom.model.Orm` child you
 print(Orm.query) # prom.query.Query
 ```
 
-Everytime you call this property, a new `prom.query.Query` instance will be created.
+Every time you call this property, a new `prom.query.Query` instance will be created.
 
 
 ## Customizing the queries
@@ -19,9 +19,9 @@ You can also extend the default `prom.query.Query` class and let your `prom.mode
 import prom
 
 class DemoQuery(prom.Query):
-    def get_by_foo(self, *foos):
+    async def get_by_foo(self, *foos):
         """get all demos with matching foos, ordered by last updated first"""
-        return self.in_foo(*foos).desc_updated().get()
+        return await self.in_foo(*foos).desc_updated().get()
 
 class DemoOrm(prom.Orm):
     query_class = DemoQuery
@@ -29,7 +29,7 @@ class DemoOrm(prom.Orm):
     foo = prom.Field(int)
 
 
-DemoOrm.query.get_by_foo(1, 2, 3) # this now works
+await DemoOrm.query.get_by_foo(1, 2, 3) # this now works
 ```
 
 Notice the `query_class` class property on the `DemoOrm` class. Now every instance of `DemoOrm` (or child that derives from it) will use `DemoQuery`.
@@ -37,7 +37,7 @@ Notice the `query_class` class property on the `DemoOrm` class. Now every instan
 
 ### The Iterator Class
 
-the `get` and `all` query methods return a `prom.query.Iterator` instance. This instance has a useful attribute `has_more` that will be true if there are more rows in the db that match the query.
+the `get` query method returns a `prom.query.Iterator` instance. This instance has a useful method `has_more` that will be true if there are more rows in the db that match the query, this can make creating paginated results easier.
 
 Similar to the Query class, you can customize the Iterator class by setting the `iterator_class` class variable:
 
@@ -111,7 +111,7 @@ You can also sort by a list of values:
 ```python
 foos = [3, 5, 2, 1]
 
-rows = query.select_foo().in_foo(foos).asc_foo(foos).values()
+rows = await query.select_foo().in_foo(foos).asc_foo(foos).tolist()
 print rows # [3, 5, 2, 1]
 ```
 
@@ -135,7 +135,7 @@ query.select("foo", "che").is_foo(10).is_bar("value 2").desc_che().limit(5).get(
 You can also write your own queries by hand:
 
 ```python
-query.raw("SELECT * FROM table_name WHERE foo = %s AND bar = %s", [10, "value 2"])
+await query.raw("SELECT * FROM table_name WHERE foo = %s AND bar = %s", [10, "value 2"])
 ```
 
 
@@ -144,30 +144,13 @@ query.raw("SELECT * FROM table_name WHERE foo = %s AND bar = %s", [10, "value 2"
 The `prom.query.Query` has a couple helpful query methods to make grabbing rows easy:
 
   * get -- `get()` -- run the select query. Return an `Iterator` instance.
-  * all -- `all()` -- alias for `get`. Return an `Iterator` instance.
   * one -- `one()` -- run the select query with a LIMIT 1. Return an `Orm` instance.
   * count -- `count()` -- return an integer of how many rows match the query, Return an integer.
-  * value -- `value()` -- similar to `one()` but only returns the selected field(s). Return a `dict`.
-  * values -- `values()` -- return an `Iterator` of the selected fields, not an `prom.model.Orm` instance
-
-    This is really handy for when you want to get all the ids as a list:
-
-    ```python
-    # get all the bar ids we want
-    bar_ids = Bar.query.select_pk().values()
-
-    # now pull out the Foo instances that correspond to the Bar ids
-    foos = Foo.query.is_bar_id(bar_ids).get()
-    ```
-
   * has -- `has()` -- return True if there is at least one row in the db matching query
-  * exists -- `exists()` -- alias for `has`
-  * one_* -- `one_<FIELDNAME>(<VALUES>)` -- run the select query with a `WHERE <FIELDNAME> = <VALUE>`, this will always return an `Orm` instance.
-  * get_* -- `get_<FIELDNAME>([<VALUE1>, <VALUE2>, ...])` -- run the select query with `WHERE <FIELDNAME> IN (...)`, this will always return an `Iterator` instance
   * raw -- `raw(query_str, *query_args, **query_options)` -- run a raw query
 
       ```python
-      Foo.query.raw("SELECT * FROM {} WHERE bar = %s".format(Foo.schema), ["bar value"])
+      await Foo.query.raw("SELECT * FROM {} WHERE bar = %s".format(Foo.schema), ["bar value"])
       ```
 
     **NOTE**, Doing custom queries using `raw` would be the only way to do join queries.
@@ -191,10 +174,10 @@ class Foo(prom.Orm):
     index_dt = prom.Index('dt')
 
 # get all the foos that have the 7th of every month
-r = q.is_dt(day=7).all() # SELECT * FROM foo_table WHERE EXTRACT(DAY FROM dt) = 7
+r = await q.is_dt(day=7).get() # SELECT * FROM foo_table WHERE EXTRACT(DAY FROM dt) = 7
 
 # get all the foos in 2013
-r = q.is_dt(year=2013).all()
+r = await q.is_dt(year=2013).get()
 ```
 
 Hopefully you get the idea from the above code.
diff --git a/docs/README_TESTING.md b/docs/README_TESTING.md
deleted file mode 100644
index 4d350de..0000000
--- a/docs/README_TESTING.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Testing
-
-Prom can be tested using [pyenv](https://github.com/pyenv/pyenv) and [tox](https://github.com/tox-dev/tox). 
-
-The current `tox.ini` file expects python 2.7 and 3.6 versions to be globally available. You can do this by running (after pyenv and tox are installed):
-
-    $ pyenv install 2.7.14
-    $ pyenv install 3.6.3
-    $ pyenv global 2.7.14 3.6.3
-
-You can verify these are now available:
-
-    $ python --version
-    Python 2.7.14
-    $ python3.6 --version
-    Python 3.6.3
-
-And then you can run `tox` to test (from the prom repository direcotry):
-
-	$ tox
-
-You can reset your environment to the default python by running:
-
-	$ pyenv global system
diff --git a/prom/__init__.py b/prom/__init__.py
index 71c6931..3bb5cf7 100644
--- a/prom/__init__.py
+++ b/prom/__init__.py
@@ -1,7 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import logging
 
 from .config import (
     DsnConnection,
@@ -24,28 +21,27 @@
     UniqueError,
     TableError,
     FieldError,
-    UniqueError,
     CloseError,
 )
 
 from . import utils
 
 
-__version__ = '4.7.0'
+__version__ = '5.0.0'
 
 
-def transaction(connection_name="", **kwargs):
+async def transaction(connection_name="", **kwargs):
     """Create a transaction 
 
-    Sometimes you just need to batch a whole bunch of operation across a whole bunch
-    of different models, this allows you to create a transaction outside of any
-    of the models so you can do that, it will yield a connection you can then pass
-    into the Orm/Query methods
+    Sometimes you just need to batch a whole bunch of operation across a whole
+    bunch of different models, this allows you to create a transaction outside
+    of any of the models so you can do that, it will yield a connection you can
+    then pass into the Orm/Query methods
 
     :Example:
-        with prom.transaction(prefix="batch") as conn:
+        async with prom.transaction(prefix="batch") as conn:
             o = FooOrm(foo=1)
-            o.save(connection=conn)
+            await o.save(connection=conn)
 
     :param connection_name: str, the connection name corresponding to the anchor
         of the DSN, defaults to the default "" connection
@@ -57,5 +53,5 @@ def transaction(connection_name="", **kwargs):
     """
     kwargs.setdefault("nest", False)
     kwargs.setdefault("prefix", f"prom_{connection_name}_tx")
-    return get_interface(connection_name).transaction(**kwargs)
+    return await get_interface(connection_name).transaction(**kwargs)
 
diff --git a/prom/config.py b/prom/config.py
index 6ffd66f..dfd26bd 100644
--- a/prom/config.py
+++ b/prom/config.py
@@ -1,19 +1,21 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import decimal
 import datetime
 import inspect
 import re
-import base64
 import json
 import logging
 import uuid
+import enum
 
 import dsnparse
 from datatypes import (
     Datetime,
     property as cachedproperty,
 )
+from datatypes.enum import (
+    find_value,
+)
 
 from .compat import *
 from . import utils
@@ -23,8 +25,7 @@
 
 
 class Connection(object):
-    """
-    set the paramaters you want to use to connect to an interface
+    """set the paramaters you want to use to connect to an interface
 
     https://github.com/Jaymon/Mingo/blob/master/Mingo/MingoConfig.php
     """
@@ -32,7 +33,11 @@ class Connection(object):
     """string -- the name of this connection (eg, Postgres, or SQLite)"""
 
     interface_name = ""
-    """string -- full Interface class name -- the interface the connection should use to talk with the db"""
+    """string -- full Interface class name -- the interface the connection
+    should use to talk with the db"""
+
+    interface_class = None
+    """Holds the interface class this connection should use"""
 
     host = ""
     """the hostname"""
@@ -63,7 +68,8 @@ def interface(self):
 
     def __init__(self, fields=None, **fields_kwargs):
         """
-        set all the values by passing them into this constructor, any unrecognized kwargs get put into .options
+        set all the values by passing them into this constructor, any
+        unrecognized kwargs get put into .options
 
         :example:
             c = Connection(
@@ -88,9 +94,14 @@ def __init__(self, fields=None, **fields_kwargs):
             else:
                 self.options[key] = val
 
-        interface_module, interface_class = utils.get_objects(self.interface_name)
+        interface_module, interface_class = utils.get_objects(
+            self.interface_name
+        )
         self.interface_class = interface_class
-        interface_class.configure(self)
+
+    def get(self, key, default=None):
+        """Works similar dict.get"""
+        return getattr(self, key, default)
 
 
 class DsnConnection(Connection):
@@ -99,9 +110,30 @@ class DsnConnection(Connection):
 
     the prom dsn is in the form:
 
-        <full.python.path.InterfaceClass>://<username>:<password>@<host>:<port>/<database>?<options=val&query=string>#<name>
+        <SCHEME>://<HOST>/<PATH>?<QUERY>#<FRAGMENT>
+
+    Where <SCHEME> is:
+
+        <full.python.path.InterfaceClass>
+
+    And <HOST> is:
+
+        ://<username>:<password>@<host>:<port>
 
-    This is useful to allow connections coming in through environment variables as described in :
+    And <PATH> is:
+
+        /<database>
+
+    And <QUERY> is:
+
+        ?<options=val&query=string>
+
+    And <FRAGMENT> is:
+
+        #<connection-name>
+
+    This is useful to allow connections coming in through environment variables
+    as described in :
         http://www.12factor.net/backing-services
 
     It tooks its inspiration from this project:
@@ -127,20 +159,23 @@ def parse(self, dsn):
         # remap certain values
         d["name"] = d.pop("fragment")
         d["interface_name"] = self.normalize_scheme(d.pop("scheme"))
-        d["database"] = d.pop("path")
+        d["database"] = parser.database
         d["options"] = d.pop("query_params", {})
         d["host"] = d.pop("hostname")
         d["readonly"] = bool(d["options"].pop("readonly", self.readonly))
 
         # get rid of certain values
         d.pop("params", None)
+
         return d
 
     def normalize_scheme(self, v):
         ret = v
         d = {
             "prom.interface.sqlite.SQLite": set(["sqlite"]),
-            "prom.interface.postgres.PostgreSQL": set(["postgres", "postgresql", "psql"])
+            "prom.interface.postgres.PostgreSQL": set(
+                ["postgres", "postgresql", "psql"]
+            )
         }
 
         kv = v.lower()
@@ -156,8 +191,8 @@ class Schema(object):
     """
     handles all table schema definition
 
-    the table schema definition includes the table name, the fields the table has, and
-    the indexes that are on the table
+    the table schema definition includes the table name, the fields the table
+    has, and the indexes that are on the table
     """
     instances = {}
     """class variable, holds different schema instances for various orms"""
@@ -181,7 +216,9 @@ def normal_fields(self):
 
     @property
     def required_fields(self):
-        """The normal required fields (eg, no magic fields like _id are included)"""
+        """The normal required fields (eg, no magic fields like _id are
+        included)
+        """
         return {f:v for f, v in self.normal_fields.items() if v.required}
 
     @property
@@ -191,7 +228,9 @@ def ref_fields(self):
 
     @property
     def magic_fields(self):
-        """the magic fields for the schema, magic fields start with an underscore"""
+        """the magic fields for the schema, magic fields start with an
+        underscore
+        """
         return {f:v for f, v in self.fields.items() if f.startswith('_')}
 
     @property
@@ -207,8 +246,10 @@ def pk_name(self):
         try:
             pk_field = self.__getattr__("pk")
             pk_name = pk_field.name
+
         except AttributeError:
             pk_name = None
+
         return pk_name
 
     @property
@@ -239,7 +280,8 @@ def get_instance(cls, orm_class, **kwargs):
         """return a Schema singleton instance for the given orm_class
 
         if there isn't already an instance in cache then a new instance will be
-        created. If a Schema instance is already in cache then it will be returned
+        created. If a Schema instance is already in cache then it will be
+        returned
 
         :param orm_class: Orm, the class to create the schema for
         :returns: Schema
@@ -291,7 +333,8 @@ def get_instance(cls, orm_class, **kwargs):
     def __init__(self, table_name, **fields_or_indexes):
         """Create an instance
 
-        every Orm should have a .schema attribute that is an instance of this class
+        every Orm should have a .schema attribute that is an instance of this
+        class
 
         :example:
             schema = Schema(
@@ -302,8 +345,8 @@ def __init__(self, table_name, **fields_or_indexes):
             )
 
         :param table_name: string, the table name
-        :param **fields_or_indexes: a dict of field name or index keys with tuple values,
-            see __getattr__ for more details
+        :param **fields_or_indexes: a dict of field name or index keys with
+            tuple values, see __getattr__ for more details
         """
         self.fields = {}
         self.indexes = {}
@@ -312,8 +355,8 @@ def __init__(self, table_name, **fields_or_indexes):
         self.lookup = {
             "field_names": {},
             # holds parent's Field.names that have been set to None in children
-            # classes. Basically, if a parent class sets foo and then a child class
-            # later on sets foo = None then foo.names will be here
+            # classes. Basically, if a parent class sets foo and then a child
+            # class later on sets foo = None then foo.names will be here
             "field_names_deleted": {},
         }
 
@@ -341,16 +384,18 @@ def set(self, name, val):
             raise TypeError("Not a Field or Index instance")
 
     def __getattr__(self, name):
-        """
-        return the Field instance for the given name
+        """return the Field instance for the given name
 
-        return -- string -- the string value of the attribute name, eg, self.foo returns "foo"
+        :returns: str, the string value of the attribute name, eg, self.foo
+            returns "foo"
         """
         try:
             return self.lookup["field_names"][name]
 
         except KeyError:
-            raise AttributeError("No {} field in schema {}".format(name, self.table_name))
+            raise AttributeError(
+                "No {} field in schema {}".format(name, self.table_name)
+            )
 
     def has_field(self, field_name):
         """Return True if schema contains field_name"""
@@ -362,8 +407,12 @@ def __contains__(self, field_name):
     def set_field(self, field_name, field):
         if not field_name:
             raise ValueError("field_name is empty")
+
         if field_name in self.fields:
-            raise ValueError(f"{field_name} already exists and cannot be changed")
+            raise ValueError(
+                f"{field_name} already exists and cannot be changed"
+            )
+
         if not isinstance(field, Field):
             raise ValueError(f"{type(field)} is not a Field instance")
 
@@ -399,18 +448,23 @@ def set_field(self, field_name, field):
     def set_index(self, index_name, index):
         """Add an index to the schema
 
-        for the most part, you will use the __getattr__ method of adding indexes for a more fluid interface,
-        but you can use this if you want to get closer to the bare metal
+        for the most part, you will use the __getattr__ method of adding indexes
+        for a more fluid interface, but you can use this if you want to get
+        closer to the bare metal
 
-        index_name -- string -- the name of the index
-        index -- Index() -- an Index instance
+        :param index_name: str, the name of the index
+        :param index: Index, an Index instance
         """
         if not index_name:
             raise ValueError("index_name must have a value")
+
         if index_name in self.indexes:
-            raise ValueError("index_name {} has already been defined on {}".format(
-                index_name, str(self.indexes[index_name].field_names)
-            ))
+            raise ValueError(
+                "index_name {} has already been defined on {}".format(
+                    index_name, str(self.indexes[index_name].field_names)
+                )
+            )
+
         if not isinstance(index, Index):
             raise ValueError(f"{type(index)} is not an Index instance")
 
@@ -426,9 +480,10 @@ def field_name(self, field_name, *default):
         most of the time, the field name of field_name will just be field_name,
         but this checks the configured aliases to return the canonical name
 
-        :param field_name: str, the field_name you want the canonical field name for
-        :param *default: mixed, if present this will be returned instead of AttributeError
-            raised if field_name doesn't exist
+        :param field_name: str, the field_name you want the canonical field name
+            for
+        :param *default: mixed, if present this will be returned instead of
+            AttributeError raised if field_name doesn't exist
         """
         try:
             return self.__getattr__(field_name).name
@@ -445,12 +500,12 @@ def field_name(self, field_name, *default):
                     raise
 
     def field_model_name(self, field_name):
-        """Check field_name against all the field's ref model names to see if there
-        is a match, this is separate from .field_name because there are times when
-        this behavior might not be desirable
+        """Check field_name against all the field's ref model names to see if
+        there is a match, this is separate from .field_name because there are
+        times when this behavior might not be desirable
 
-        :param field_name: str, the field/model name you want the canonical field
-            name for
+        :param field_name: str, the field/model name you want the canonical
+            field name for
         :returns: str, the canonical field name
         """
         for fn, field in self.fields.items():
@@ -460,13 +515,14 @@ def field_model_name(self, field_name):
         raise AttributeError(field_name)
 
     def create_orm(self, orm_class=None):
-        """If you have a schema but don't have an Orm for it, you can call this method
-        and have an orm_class created that will have the fields and table_name of this
-        schema
-
-        :param orm_class: Orm, if you want your generated class to have a certain
-            parent class you can pass in the parent class you want
-        :returns: Orm, your child orm populated with information from this schema
+        """If you have a schema but don't have an Orm for it, you can call this
+        method and have an orm_class created that will have the fields and
+        table_name of this schema
+
+        :param orm_class: Orm, if you want your generated class to have a
+            certain parent class you can pass in the parent class you want
+        :returns: Orm, your child orm populated with information from this
+            schema
         """
         if not orm_class:
             from .model import Orm # avoid circular dependency
@@ -485,9 +541,7 @@ def create_orm(self, orm_class=None):
             for k, v in vars(klass).items():
                 if k not in seen_properties:
                     if isinstance(v, (Field, Index)):
-                        #setattr(child_class, k, property(lambda: AttributeError))
                         setattr(child_class, k, None)
-                        #delattr(child_class, k)
 
                     elif isinstance(v, type) and issubclass(v, Field):
                         setattr(child_class, k, None)
@@ -501,16 +555,15 @@ def create_orm(self, orm_class=None):
 
 
 class Index(object):
-    """Each index on the table is configured using this class"""
-
+    """Each index on the table is configured using this class
+    """
     def __init__(self, *field_names, **options):
-        """
-        initialize an index
+        """initialize an index
 
-        index_fields -- list -- the string field_names this index will index on, fields have to be already added
-            to this schema index
-        **options -- dict --
-            unique -- boolean -- True if the index should be unique, false otherwise
+        :param *field_names: the string field names this index will index on,
+            fields have to be already added to this schema index
+        :param **options:
+            - unique: bool, True if the index should be unique, false otherwise
         """
         if not field_names:
             raise ValueError("field_names list is empty")
@@ -524,24 +577,31 @@ def __init__(self, *field_names, **options):
 class FieldMeta(type):
     """Allows a class definition to be a descriptor also
 
-    I don't love this solution, but I like the syntax of just being able to define
-    a subclass inside an Orm and have it work. The problem is an embedded class isn't,
-    by default, a descriptor instance, so it wasn't calling fset, fget, and fdel.
+    I don't love this solution, but I like the syntax of just being able to
+    define a subclass inside an Orm and have it work. The problem is an embedded
+    class isn't, by default, a descriptor instance, so it wasn't calling fset,
+    fget, and fdel.
 
     This makes it so an embedded field class will be treated as a descriptor. It
     uses an embedded .instance property to actually perform the operations
     """
     def __set_name__(cls, orm_class, name):
-        """I'm not quite sure why this works, but it does. Basically, when you define
-        the field as an embedded class in the orm, this method will get called
-        when the class is being parsed/loaded the first time.
+        """I'm not quite sure why this works, but it does. Basically, when you
+        define the field as an embedded class in the orm, this method will get
+        called when the class is being parsed/loaded the first time.
 
         So this method gets called and we use cls to create a Field instance and
-        we then set our newly created instance onto orm_class.name. So after this
-        method is called, orm_class.name will be a Field instance instead of a
-        Field subclass
+        we then set our newly created instance onto orm_class.name. So after
+        this method is called, orm_class.name will be a Field instance instead
+        of a Field subclass
         """
-        logger.debug(f"Creating field descriptor {orm_class.__name__}.{name} with class {cls.__name__}")
+        logger.debug(
+            "Creating field descriptor {}.{} with class {}".format(
+                orm_class.__name__,
+                name,
+                cls.__name__
+            )
+        )
 
         instance = cls(cls.type, cls.required, cls.options)
         instance.__set_name__(orm_class, name)
@@ -555,8 +615,8 @@ class Field(object, metaclass=FieldMeta):
     control over how a field in the db is set and fetched from the instance and
     also how it is set and fetched from the interface
 
-    to customize the field on set and get from the db, you can use the decorators,
-    which are classmethods:
+    to customize the field on set and get from the db, you can use the
+    decorators, which are classmethods:
 
     :Example:
         foo = Field(str, True)
@@ -572,8 +632,8 @@ def foo(self, val):
             return val
 
     There are also fget/fset/fdel methods that can be set to customize behavior
-    on when a value is set on a particular orm instance, so if you wanted to make sure
-    that bar was always an int when it is set, you could:
+    on when a value is set on a particular orm instance, so if you wanted to
+    make sure that bar was always an int when it is set, you could:
 
     :Example:
         bar = Field(int, True)
@@ -592,12 +652,13 @@ def bar(self, name, val):
             # convert val to something json safe, you can also change the name
             return name, val
 
-    NOTE -- all these get/set/delete methods are different than traditional python getters
-    and setters because they always need to return a value and they always take in the
-    object instance manipulating them and the value.
+    NOTE -- all these get/set/delete methods are different than traditional
+    python getters and setters because they always need to return a value and
+    they always take in the object instance manipulating them and the value.
 
     NOTE -- Foreign key's can be passed orm instances from other classes because
-    those classes will call the FK's field methods when getting/setting the field
+    those classes will call the FK's field methods when getting/setting the
+    field
 
     You can also configure the Field using a class:
 
@@ -641,8 +702,8 @@ def jsonable(self, orm, field_name, v):
     """True if this field is required"""
 
     options = None
-    """In the instance, this will be a dict of key/val pairs containing extra information
-    about the field"""
+    """In the instance, this will be a dict of key/val pairs containing extra
+    information about the field"""
 
     default = None
     """Default value for this field"""
@@ -654,7 +715,8 @@ def jsonable(self, orm, field_name, v):
     """The field name"""
 
     choices = None
-    """A set of values that this field can be, if set then no other values can be set"""
+    """A set of values that this field can be, if set then no other values can
+    be set"""
 
     unique = False
     """True if this field is unique indexed"""
@@ -667,15 +729,19 @@ def jsonable(self, orm, field_name, v):
 
     @cachedproperty(cached="_schema")
     def schema(self):
-        """return the schema instance if this field is a reference to another table
+        """return the schema instance if this field is a reference to another
+        table
 
-        see .set_type() for an explanation on why we defer figuring this out until now
+        see .set_type() for an explanation on why we defer figuring this out
+        until now
         """
         field_type = self.original_type
         module, klass = utils.get_objects(field_type)
         schema = klass.schema
         if not schema:
-            raise ValueError("Field type {} is not an Orm class".format(field_type))
+            raise ValueError(
+                "Field type {} is not an Orm class".format(field_type)
+            )
 
         return schema
 
@@ -688,7 +754,8 @@ def type(self):
 
     @property
     def interface_type(self):
-        """Returns the type that will be used in the interface to create the table
+        """Returns the type that will be used in the interface to create the
+        table
 
         see .set_type() for an explanation on why we defer this until here
         """
@@ -764,35 +831,47 @@ def interface_options(self):
             options = self.options
         return options
 
-    def __init__(self, field_type, field_required=False, field_options=None, **field_options_kwargs):
+    def __init__(
+        self,
+        field_type,
+        field_required=False,
+        field_options=None,
+        **field_options_kwargs
+    ):
         """
         create a field
 
         :param field_type: type, the python type of the field, so for a string
             you would pass str, integer: int, boolean: bool, float: float
-        :param field_required: boolean, true if this field has to be there to insert
-        :param field_options: dict, everything else in key: val notation. Current options:
+        :param field_required: boolean, true if this field has to be there to
+            insert
+        :param field_options: Current options:
             * size: int -- the size you want the string to be, or the int to be
             * min_size: int -- the minimum size
             * max_size: int -- if you want a varchar, set this
-            * unique: bool -- True to set a unique index on this field, this is just for convenience and is
-                equal to self.set_index(field_name, [field_name], unique=True). this is a convenience option
-                to set a unique index on the field without having to add a separate index statement
-            * ignore_case: bool -- True to ignore case if this field is used in indexes
-            * default: mixed -- defaults to None, can be anything the db can support
-            * jsonable_name: str, the name of the field when Orm.jsonable() is called
+            * unique: bool -- True to set a unique index on this field, this is
+                just for convenience and is equal to self.set_index(field_name,
+                [field_name], unique=True). this is a convenience option to set
+                a unique index on the field without having to add a separate
+                index statement
+            * ignore_case: bool -- True to ignore case if this field is used in
+                indexes
+            * default: mixed -- defaults to None, can be anything the db can
+                support
+            * jsonable_name: str, the name of the field when Orm.jsonable() is
+                called
             * jsonable_field: bool, if False then this field won't be part of
                 Orm.jsonable() output
             * jsonable: str|callable|bool,
                 - str, will be set to jsonable_name
                 - callable, will be used as self.jsonable
                 - bool, will set jsonable_field to False
-            * empty: bool, (default is True), set to False if the value cannot be
-                empty when being sent to the db (empty is None, "", 0, False)
+            * empty: bool, (default is True), set to False if the value cannot
+                be empty when being sent to the db (empty is None, "", 0, False)
             * pk: bool, True to make this field the primary key
-            * auto: bool, True to tag this field as an auto-generated field. Used
-                with an int to set it to an auto-increment, use it with UUID to
-                have uuids auto generated
+            * auto: bool, True to tag this field as an auto-generated field.
+                Used with an int to set it to an auto-increment, use it with
+                UUID to have uuids auto generated
             * autopk: bool, shortcut for setting pk=True, auto=True
         :param **field_options_kwargs: dict, will be combined with field_options
         """
@@ -867,7 +946,11 @@ def __set_name__(self, orm_class, name):
 
         if orm_class:
             logger.debug(
-                f"Field descriptor {orm_class.__name__}.{name} created with internal name {self.orm_field_name}"
+                "Field descriptor {}.{} created with internal name {}".format(
+                    orm_class.__name__,
+                    name,
+                    self.orm_field_name
+                )
             )
 
     def get_size(self, field_options):
@@ -895,12 +978,13 @@ def get_size(self, field_options):
         return d
 
     def size_info(self):
-        """Figure out actual sizing information with all the ways we can calculate
-        size now
+        """Figure out actual sizing information with all the ways we can
+        calculate size now
 
         :returns: dict, will always have "has_size" and "has_precision" keys
             * if "has_size" key is True then "size" key will exist
-            * if "has_precision" is True then "precision" and "scale" keys will exist
+            * if "has_precision" is True then "precision" and "scale" keys will
+                exist
             * if "bounds" key exists it will be a tuple (min_size, max_size)
         """
         ret = {
@@ -989,44 +1073,54 @@ def size_info(self):
     def set_type(self, field_type):
         """Try to infer as much about the type as can be inferred at this moment
 
-        Because the Field supports string classpaths (eg, "modname.Classname") we can't
-        figure everything out in this method, so we figure out as much as we can
-        and then defer everything else to the .interface_type and .schema properties, this
-        allows the parser to hopefully finish loading the modules before we have to
-        parse the classpath to find the foreign key schema
+        Because the Field supports string classpaths (eg, "modname.Classname")
+        we can't figure everything out in this method, so we figure out as much
+        as we can and then defer everything else to the .interface_type and
+        .schema properties, this allows the parser to hopefully finish loading
+        the modules before we have to parse the classpath to find the foreign
+        key schema
 
         :param field_type: mixed, the field type passed into __init__
         """
-        std_types = (
-            str,
-            bool,
-            int,
-            float,
-            bytes,
-            bytearray,
-            decimal.Decimal,
-            datetime.datetime,
-            datetime.date,
-            uuid.UUID,
-            dict,
-        )
-
-        json_types = ()
-
-        pickle_types = (
-            set,
-            list,
-        )
-
         self.original_type = field_type
         self.serializer = ""
         self._interface_type = None
         self._schema = None
 
         if isinstance(field_type, type):
+            std_types = (
+                str,
+                bool,
+                int,
+                float,
+                bytes,
+                bytearray,
+                decimal.Decimal,
+                datetime.datetime,
+                datetime.date,
+                uuid.UUID,
+                dict,
+            )
+
+            json_types = ()
+
+            pickle_types = (
+                set,
+                list,
+            )
+
             if issubclass(field_type, std_types):
                 self._interface_type = field_type
 
+            elif issubclass(field_type, enum.Enum):
+                # https://docs.python.org/3/library/enum.html
+                self.iquerier(self._iquery_enum)
+                self.fsetter(self._fset_enum)
+
+                for enum_property in field_type:
+                    self._interface_type = type(enum_property.value)
+                    break
+
             elif issubclass(field_type, json_types):
                 self.serializer = self.options.pop("serializer", "json")
                 self._interface_type = str
@@ -1068,7 +1162,8 @@ def is_auto(self):
         return self.options.get("auto", False)
 
     def is_ref(self):
-        """return true if this field foreign key references the primary key of another orm"""
+        """return true if this field foreign key references the primary key of
+        another orm"""
         return bool(self.schema)
 
     def is_required(self):
@@ -1098,13 +1193,15 @@ def fgetter(self, v):
         return self
 
     def iget(self, orm, val):
-        """Called anytime the field is being returned from the interface to the orm
+        """Called anytime the field is being returned from the interface to the
+        orm
 
-        think of this as when the orm receives the field value from the interface
+        think of this as when the orm receives the field value from the
+        interface
 
         :param orm: Orm, the Orm instance the field is being set on. This can be
-            None if the select query had selected fields so a full orm instance isn't
-            being returned but rather just the selected values
+            None if the select query had selected fields so a full orm instance
+            isn't being returned but rather just the selected values
         :param val: mixed, the current value of the field
         :returns: mixed
         """
@@ -1123,15 +1220,17 @@ def iget(self, orm, val):
         return val
 
     def igetter(self, v):
-        """decorator for the method called when a field is pulled from the database"""
+        """decorator for the method called when a field is pulled from the
+        database"""
         self.iget = v
         return self
 
     def fset(self, orm, val):
-        """This is called on Orm instantiation and any time field is set (eg Orm.foo = ...)
+        """This is called on Orm instantiation and any time field is set (eg
+        Orm.foo = ...)
 
-        on Orm creation val will be None if the field wasn't passed to Orm.__init__ otherwise
-        it will be the value passed into Orm.__init__
+        on Orm creation val will be None if the field wasn't passed to
+        Orm.__init__ otherwise it will be the value passed into Orm.__init__
 
         :param orm: Orm, the Orm instance the field is being set on
         :param val: mixed, the current value of the field
@@ -1140,11 +1239,16 @@ def fset(self, orm, val):
         logger.debug(f"fset {orm.__class__.__name__}.{self.name}")
         if val is not None:
             if self.choices and val not in self.choices:
-                raise ValueError("Value {} not in {} value choices".format(val, self.name))
+                raise ValueError("Value {} not in {} value choices".format(
+                    val,
+                    self.name
+                ))
 
             if regex := self.options.get("regex", ""):
                 if not re.search(regex, val):
-                    raise ValueError(f"regex failed for {orm.__class__.__name__}.{self.name}")
+                    raise ValueError(
+                        f"regex failed for {orm.__class__.__name__}.{self.name}"
+                    )
 
         if self.is_ref():
             # Foreign Keys get passed through their Field methods
@@ -1152,6 +1256,13 @@ def fset(self, orm, val):
 
         return val
 
+    def _fset_enum(self, orm, val):
+        """This is set using .fsetter in .set_type when an Enum is identified"""
+        if val is not None:
+            val = find_value(self.original_type, val)
+
+        return val
+
     def fsetter(self, v):
         """decorator for setting field's fset function"""
         self.fset = v
@@ -1162,8 +1273,8 @@ def iset(self, orm, val):
 
         think of this as when the interface is going to get the field value or
         when the field is being sent to the db. Alot of the value checks like
-        required and empty are in Orm.to_interface(), this just returns the value
-        and nothing else
+        required and empty are in Orm.to_interface(), this just returns the
+        value and nothing else
 
         :param orm: Orm
         :param val: Any, the current value of the field
@@ -1264,8 +1375,17 @@ def iquery(self, query, val):
             val = self.schema.pk.iquery(query, val)
         return val
 
+    def _iquery_enum(self, query, val):
+        """This is set using .iquerier in .set_type when an Enum is identified
+        """
+        if val is not None:
+            val = find_value(self.original_type, val)
+
+        return val
+
     def iquerier(self, v):
-        """decorator for the method called when this field is used in a SELECT query"""
+        """decorator for the method called when this field is used in a SELECT
+        query"""
         self.iquery = v
         return self
 
@@ -1273,14 +1393,16 @@ def jsonable(self, orm, name, val):
         """This is called in Orm.jsonable() to set the field name nad value
 
         :param orm: Orm, the instance currently calling jsonable
-        :param name: str, the Orm field name that will be used if the jsonable_name
-            option isn't set
+        :param name: str, the Orm field name that will be used if the
+            jsonable_name option isn't set
         :param val: Any, the field's value
-        :returns: tuple[str, Any], (name, val) where the name will be the jsonable
-            field name and the value will be the jsonable value
+        :returns: tuple[str, Any], (name, val) where the name will be the
+            jsonable field name and the value will be the jsonable value
         """
         if self.options.get("jsonable_field", True):
-            logger.debug(f"jsonable {orm.__class__.__name__}.{self.name} -> {name}")
+            logger.debug(
+                f"jsonable {orm.__class__.__name__}.{self.name} -> {name}"
+            )
 
             if self.is_ref():
                 # Foreign Keys get passed through their Field methods
@@ -1298,12 +1420,14 @@ def jsonable(self, orm, name, val):
             return self.options.get("jsonable_name", name), val
 
         else:
-            logger.debug(f"jsonable ignored for {orm.__class__.__name__}.{self.name}")
+            logger.debug(
+                f"jsonable ignored for {orm.__class__.__name__}.{self.name}"
+            )
             return "", None
 
     def jsonabler(self, v):
-        """Decorator for the method called for a field when an Orm's .jsonable method
-        is called"""
+        """Decorator for the method called for a field when an Orm's .jsonable
+        method is called"""
         self.jsonable = v
         return self
 
@@ -1330,8 +1454,10 @@ def modified(self, orm, val):
     def hash(self, orm, val):
         if self.is_serialized():
             return None
+
         elif isinstance(val, dict):
             return None
+
         return hash(val)
 
     def encode(self, val):
@@ -1361,7 +1487,8 @@ def decode(self, val):
             raise ValueError("Unknown serializer {}".format(self.serializer))
 
     def fval(self, orm):
-        """return the raw value that this property is holding internally for the orm instance"""
+        """return the raw value that this property is holding internally for the
+        orm instance"""
         try:
             val = orm.__dict__[self.orm_field_name]
 
@@ -1373,9 +1500,9 @@ def fval(self, orm):
 
     def __get__(self, orm, orm_class=None):
         """This is the wrapper that will actually be called when the field is
-        fetched from the instance, this is a little different than Python's built-in
-        @property fget method because it will pull the value from a shadow variable in
-        the instance and then call fget"""
+        fetched from the instance, this is a little different than Python's
+        built-in @property fget method because it will pull the value from a
+        shadow variable in the instance and then call fget"""
         if orm is None:
             # class is requesting this property, so return it
             return self
@@ -1394,23 +1521,25 @@ def __get__(self, orm, orm_class=None):
 
     def __set__(self, orm, val):
         """this is the wrapper that will actually be called when the field is
-        set on the instance, your fset method must return the value you want set,
-        this is different than Python's built-in @property setter because the
-        fset method *NEEDS* to return something"""
+        set on the instance, your fset method must return the value you want
+        set, this is different than Python's built-in @property setter because
+        the fset method *NEEDS* to return something
+        """
         val = self.fset(orm, val)
         orm.__dict__[self.orm_field_name] = val
 
     def __delete__(self, orm):
-        """the wrapper for when the field is deleted, for the most part the default
-        fdel will almost never be messed with, this is different than Python's built-in
-        @property deleter because the fdel method *NEEDS* to return something and it
-        accepts the current value as an argument"""
+        """the wrapper for when the field is deleted, for the most part the
+        default fdel will almost never be messed with, this is different than
+        Python's built-in @property deleter because the fdel method *NEEDS* to
+        return something and it accepts the current value as an argument"""
         val = self.fdel(orm, self.fval(orm))
         orm.__dict__[self.orm_field_name] = val
 
 
 class AutoUUID(Field):
-    """an auto-generating UUID field, by default this will be set as primary key"""
+    """an auto-generating UUID field, by default this will be set as primary key
+    """
     def __init__(self, **kwargs):
         kwargs.setdefault("pk", True)
         kwargs.setdefault("auto", True)
@@ -1423,7 +1552,8 @@ def __init__(self, **kwargs):
 
 
 class AutoIncrement(Field):
-    """an auto-incrementing Serial field, by default this will be set as primary key"""
+    """an auto-incrementing Serial field, by default this will be set as primary
+    key"""
     def __init__(self, **kwargs):
         kwargs.setdefault("pk", True)
         kwargs.setdefault("auto", True)
@@ -1446,10 +1576,10 @@ class AutoDatetime(Field):
         updated = AutoDatetime(updated=True)
 
     The triggers are:
-        created: create a datetime in the field when the Orm is being created (added
-            to the db for the first time)
-        updated: update the datetime when the Orm is created and when it is updated
-            in the db
+        created: create a datetime in the field when the Orm is being created
+            (added to the db for the first time)
+        updated: update the datetime when the Orm is created and when it is
+            updated in the db
     """
     def __init__(self, **kwargs):
         kwargs.setdefault("created", kwargs.get("create", True))
@@ -1476,21 +1606,15 @@ def iset(self, orm, val):
 
     def created_iset(self, orm, val):
         if not val and orm.is_insert():
-            #val = datetime.datetime.utcnow()
-            #val = datetime.datetime.now(datetime.timezone.utc)
             val = Datetime()
         return val
 
     def updated_iset(self, orm, val):
         if val:
             if not self.modified(orm, val):
-                #val = datetime.datetime.utcnow()
-                #val = datetime.datetime.now(datetime.timezone.utc)
                 val = Datetime()
 
         else:
-            #val = datetime.datetime.utcnow()
-            #val = datetime.datetime.now(datetime.timezone.utc)
             val = Datetime()
 
         return val
diff --git a/prom/exception.py b/prom/exception.py
index cdcfc0e..151342f 100644
--- a/prom/exception.py
+++ b/prom/exception.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 
 
 class Error(Exception):
@@ -56,22 +55,10 @@ class CloseError(InterfaceError):
 
 
 class PlaceholderError(InterfaceError):
-    """This is raised when there is a raw query mismatch between the placeholders
-    and the passed in arguments, see Interface._raw
+    """This is raised when there is a raw query mismatch between the
+    placeholders and the passed in arguments, see Interface._raw
 
     https://github.com/Jaymon/prom/issues/74
     """
-    def __init__(self, e, query_str, query_args, placeholder, **kwargs):
-        if "message" not in kwargs:
-            count = query_str.count(placeholder)
-            kwargs["message"] = "Query has {} placeholder(s) but {} value(s)".format(
-                count,
-                len(query_args),
-            )
-
-        super().__init__(e, **kwargs)
-
-        self.query_str = query_str
-        self.query_args = query_args
-        self.placeholder = placeholder
+    pass
 
diff --git a/prom/extras/config.py b/prom/extras/config.py
deleted file mode 100644
index 6a77397..0000000
--- a/prom/extras/config.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from datatypes import Enum
-
-from ..config import (
-    Field as BaseField,
-)
-
-
-class Field(BaseField):
-    """Adds support for Enum as the field type. This allows you to set an enum string
-    value, integer value, or Enum instance to the Orm or the Query and it will 
-    be inserted into the db as an integer"""
-    def is_enum(self):
-        """Return True if the field type is an Enum"""
-        try:
-            ret = issubclass(self.original_type, Enum)
-        except TypeError:
-            ret = False
-        return ret
-
-    def fset(self, orm, val):
-        if val is not None and self.is_enum():
-            val = self.original_type.find_value(val)
-        else:
-            val = super(Field, self).fset(orm, val)
-        return val
-
-    def iquery(self, query, val):
-        if val is not None and self.is_enum():
-            val = self.original_type.find_value(val)
-        else:
-            val = super(Field, self).iquery(query, val)
-        return val
-
-    def set_type(self, field_type):
-        super(Field, self).set_type(field_type)
-        if self.is_enum():
-            self.serializer = ""
-            self._interface_type = int
-
diff --git a/prom/extras/model.py b/prom/extras/model.py
index 768b971..b847004 100644
--- a/prom/extras/model.py
+++ b/prom/extras/model.py
@@ -1,13 +1,8 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 
 from datatypes import classproperty
 
 from ..model import Orm
-from ..config import (
-    Field,
-    Index,
-)
 
 
 class MagicOrm(Orm):
@@ -26,15 +21,16 @@ def __pout__(self):
     def pk_name(cls):
         """return the preferred primary key name for the Orm
 
-        you can access the primary key on an instance using ._id or .pk but usually
-        when we have foreign keys we use modelname_id and then in the jsonable
-        dicts that are passed down we include that modelname_id in the actual instance
-        instead of an _id field (eg, the jsonable on object Foo would have 'foo_id' key)
-        and this makes it easy to get that key name and override it if needed
+        you can access the primary key on an instance using ._id or .pk but
+        usually when we have foreign keys we use modelname_id and then in the
+        jsonable dicts that are passed down we include that modelname_id in the
+        actual instance instead of an _id field (eg, the jsonable on object Foo
+        would have 'foo_id' key) and this makes it easy to get that key name and
+        override it if needed
 
-        :returns: string, the preferred primary key name that is usually the name
-            that should be used in jsonable and in foreign key fields on other
-            models
+        :returns: string, the preferred primary key name that is usually the
+            name that should be used in jsonable and in foreign key fields on
+            other models
         """
         return f"{cls.model_name}_id"
 
diff --git a/prom/interface/__init__.py b/prom/interface/__init__.py
index afb4ea9..0ea0371 100644
--- a/prom/interface/__init__.py
+++ b/prom/interface/__init__.py
@@ -19,14 +19,16 @@ def configure_environ(dsn_env_name='PROM_DSN', connection_class=DsnConnection):
     """
     configure interfaces based on environment variables
 
-    by default, when prom is imported, it will look for PROM_DSN, and PROM_DSN_N (where
-    N is 1 through infinity) in the environment, if it finds them, it will assume they
-    are dsn urls that prom understands and will configure db connections with them. If you
-    don't want this behavior (ie, you want to configure prom manually) then just make sure
-    you don't have any environment variables with matching names
+    by default, when prom is imported, it will look for PROM_DSN, and PROM_DSN_N
+    (where N is 1 through infinity) in the environment, if it finds them, it
+    will assume they are dsn urls that prom understands and will configure db
+    connections with them. If you don't want this behavior (ie, you want to
+    configure prom manually) then just make sure you don't have any environment
+    variables with matching names
 
-    The num checks (eg PROM_DSN_1, PROM_DSN_2) go in order, so you can't do PROM_DSN_1, PROM_DSN_3,
-    because it will fail on _2 and move on, so make sure your num dsns are in order (eg, 1, 2, 3, ...)
+    The num checks (eg PROM_DSN_1, PROM_DSN_2) go in order, so you can't do
+    PROM_DSN_1, PROM_DSN_3, because it will fail on _2 and move on, so make
+    sure your num dsns are in order (eg, 1, 2, 3, ...)
 
     example --
         export PROM_DSN_1=some.Interface://host:port/dbname#i1
@@ -50,10 +52,12 @@ def configure(dsn, connection_class=DsnConnection):
     """
     configure an interface to be used to query a backend
 
-    you use this function to configure an Interface using a dsn, then you can get
-    that interface using the get_interface() method
+    you use this function to configure an Interface using a dsn, then you can
+    get that interface using the get_interface() method
 
-    dsn -- string -- a properly formatted prom dsn, see DsnConnection for how to format the dsn
+    :param dsn: str, a properly formatted prom dsn, see DsnConnection for how
+        to format the dsn
+    :param connection_class: type, the class that will hold the parsed result
     """
     c = dsnparse.parse(dsn, parse_class=connection_class)
     inter = c.interface
@@ -72,15 +76,19 @@ def get_interfaces():
 
 def set_interface(interface, name=''):
     """
-    don't want to bother with a dsn? Use this method to make an interface available
+    don't want to bother with a dsn? Use this method to make an interface
+    available
     """
     global interfaces
 
-    if not interface: raise ValueError('interface is empty')
+    if not interface:
+        raise ValueError('interface is empty')
 
     # close down the interface before we discard it
     if name in interfaces:
-        interfaces[name].close()
+        pout.v(interfaces[name].is_connected())
+        if interfaces[name].is_connected():
+            raise ValueError("Cannot replace an open interface")
 
     interfaces[name] = interface
 
diff --git a/prom/interface/base.py b/prom/interface/base.py
index ab88685..4f51c4b 100644
--- a/prom/interface/base.py
+++ b/prom/interface/base.py
@@ -1,64 +1,52 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import sys
-import os
-import datetime
-import decimal
-import logging
-from contextlib import contextmanager
+from contextlib import asynccontextmanager
 import uuid
-import weakref
 
 from datatypes import (
     LogMixin,
     Stack,
 )
 
-# first party
+from ..compat import *
 from ..query import Query
 from ..exception import (
     InterfaceError,
     UniqueError,
     TableError,
     FieldError,
-    UniqueError,
     CloseError,
 )
 
-from ..compat import *
-from ..utils import make_list
-
-
-logger = logging.getLogger(__name__)
-
 
 class ConnectionABC(LogMixin):
-    """Subclasses should extend Connection and implement the methods in this class"""
-    def _transaction_start(self):
+    """Subclasses should extend Connection and implement the methods in this
+    class
+    """
+    async def _transaction_start(self):
         """Called when the first transaction is started"""
         pass
 
-    def _transaction_starting(self, tx):
+    async def _transaction_starting(self, tx):
         """Called when a nested transaction is started"""
         pass
 
-    def _transaction_ignoring(self, tx):
+    async def _transaction_ignoring(self, tx):
         """Called when nested transaction is ignored instead of started"""
         pass
 
-    def _transaction_stop(self):
+    async def _transaction_stop(self):
         """Called when the last transaction is stopped"""
         pass
 
-    def _transaction_stopping(self, tx):
+    async def _transaction_stopping(self, tx):
         """Called when a nested transaction is stopped"""
         pass
 
-    def _transaction_fail(self):
+    async def _transaction_fail(self):
         """Called when the last transaction is failed"""
         pass
 
-    def _transaction_failing(self, tx):
+    async def _transaction_failing(self, tx):
         """Called when a nested transaction is failed"""
         pass
 
@@ -68,13 +56,13 @@ class Connection(ConnectionABC):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        # counting semaphore, greater than 0 if in a transaction, 0 if no current
-        # transaction.
+        # counting-ish semaphore, length will be greater than 0 if in a
+        # transaction, and 0 if there are no current transactions.
         #
-        # This will be incremented everytime transaction_start() is called, and
-        # decremented everytime transaction_stop() is called.
+        # This will push every time transaction_start() is called, and
+        # pop every time transaction_stop() is called.
         #
-        # transaction_fail will set this back to 0 and rollback the transaction
+        # transaction_fail will clear this and rollback the transaction
         #
         # Holds the active transactions
         self.transactions = Stack()
@@ -83,8 +71,23 @@ def transaction_count(self):
         """How many active transactions there currently are"""
         return len(self.transactions)
 
-    def transaction_names(self):
-        return " > ".join((r["name"] for r in reversed(self.transactions)))
+    def transaction_names(self, tx=None):
+        """Get all the transaction names for logging
+
+        :param tx: dict, sometimes you pop the transaction before you get the
+            names, passing that popped tx will allow it to be placed at the
+            end
+        :returns: str, all the names nested
+        """
+        names = " > ".join((r["name"] for r in reversed(self.transactions)))
+
+        if tx:
+            if names:
+                names += " > "
+
+            names += tx["name"]
+
+        return names
 
     def transaction_name(self, **kwargs):
         """generate a random transaction name for use in start_transaction() and
@@ -99,9 +102,9 @@ def transaction_name(self, **kwargs):
             name = f"{prefix}_{suffix}"
         return name
 
-    def transaction_create(self, **kwargs):
-        """Create a new transaction dict that will be placed on the .transactions
-        stack
+    def transaction_info(self, **kwargs):
+        """Create a new transaction dict that will be placed on the
+        .transactions stack
 
         :param **kwargs:
             - nest: bool, True if this (and children unless passed in) will run
@@ -150,12 +153,13 @@ def transaction_exists(self):
         """
         return self.transaction_count() > 0
 
-    def transaction_start(self, **kwargs):
+    async def transaction_start(self, **kwargs):
         """start a transaction
 
-        this will increment transaction semaphore and pass it to _transaction_start()
+        this will increment transaction semaphore and pass it to
+        _transaction_start()
         """
-        tx = self.transaction_create(**kwargs)
+        tx = self.transaction_info(**kwargs)
         self.transactions.push(tx)
 
         transaction_count = tx["index"]
@@ -164,7 +168,7 @@ def transaction_start(self, **kwargs):
                 f"{transaction_count}.",
                 f"Ignoring {self} transaction {self.transaction_names()}",
             ])
-            self._transaction_ignoring(tx)
+            await self._transaction_ignoring(tx)
 
         else:
             self.log_debug([
@@ -172,14 +176,14 @@ def transaction_start(self, **kwargs):
                 f"Start {self} transaction {self.transaction_names()}",
             ])
             if transaction_count == 1:
-                self._transaction_start()
+                await self._transaction_start()
 
             else:
-                self._transaction_starting(tx)
+                await self._transaction_starting(tx)
 
         return transaction_count
 
-    def transaction_stop(self):
+    async def transaction_stop(self):
         """stop/commit a transaction if ready"""
         transaction_count = self.transaction_count()
         if transaction_count > 0:
@@ -187,18 +191,19 @@ def transaction_stop(self):
             if not tx["ignored"]:
                 self.log_debug([
                     f"{transaction_count}.", 
-                    f"Stopping {self} transaction {self.transaction_names()}",
+                    f"Stopping {self} transaction",
+                    self.transaction_names(tx),
                 ])
 
                 if transaction_count == 1:
-                    self._transaction_stop()
+                    await self._transaction_stop()
 
                 else:
-                    self._transaction_stopping(tx)
+                    await self._transaction_stopping(tx)
 
         return self.transaction_count
 
-    def transaction_fail(self):
+    async def transaction_fail(self):
         """rollback a transaction if currently in one"""
         transaction_count = self.transaction_count()
         if transaction_count > 0:
@@ -206,112 +211,137 @@ def transaction_fail(self):
             if not tx["ignored"]:
                 self.log_debug([
                     f"{transaction_count}.",
-                    f"Failing {self} transaction {self.transaction_names()}",
+                    f"Failing {self} transaction",
+                    self.transaction_names(tx),
                 ])
 
                 if transaction_count == 1:
-                    self._transaction_fail()
+                    await self._transaction_fail()
 
                 else:
-                    self._transaction_failing(tx)
+                    await self._transaction_failing(tx)
 
     def __str__(self):
         return f"0x{id(self):02x}"
 
 
 class InterfaceABC(LogMixin):
-    """This is just a convenience abstract base class so child interfaces can easily
-    see what methods they need to implement. They should extend Interface and then
-    implement the methods in this class"""
-    def _connect(self, config):
+    """This is just a convenience abstract base class so child interfaces can
+    easily see what methods they might need to implement. They should extend
+    Interface and then implement the methods in this class
+    """
+    @classmethod
+    async def configure(cls, config):
+        """This is called by Config whenever it parses a DSN, it is meant to
+        customize any configuration for the specific child interface
+        """
+        return config
+
+    async def _connect(self, config):
+        """See the docblock for Interface.connect to understand how the
+        connection interface is used by Interface to make and manage connections
+        """
         raise NotImplementedError()
 
-    def _configure_connection(self, **kwargs):
-        """This is ran immediately after a successful .connect()"""
+    async def _configure_connection(self, **kwargs):
+        """The wrapper method for this (Interface.configure_connection) is
+        never called directly by Interface. It is up to the child interfaces
+        to decide when a connection is ready and to call this method"""
         pass
 
-    def _free_connection(self, connection):
+    async def _free_connection(self, connection):
+        """The wrapper method for this method is called at the end of the
+        Interface.connection context manager"""
         pass
 
-    def _get_connection(self):
+    async def _get_connection(self):
+        """The wrapper method for this method is called at the beginning of the
+        Interface.connection context manager"""
         raise NotImplementedError()
 
-    def _close(self):
+    async def _close(self):
         raise NotImplementedError()
 
-    def _readonly(self, readonly, **kwargs):
+    async def _readonly(self, readonly, **kwargs):
         raise NotImplementedError()
 
-    def _raw(self, query_str, *query_args, **kwargs):
+    async def _raw(self, query_str, *query_args, **kwargs):
         raise NotImplementedError()
 
-    def _set_table(self, schema, **kwargs):
+    async def _set_table(self, schema, **kwargs):
         raise NotImplementedError()
 
-    def _get_tables(self, table_name, **kwargs):
+    async def _get_tables(self, table_name, **kwargs):
         raise NotImplementedError()
 
-    def _delete_table(self, schema):
+    async def _delete_table(self, schema):
         raise NotImplementedError()
 
-    def _get_fields(self, table_name, **kwargs):
+    async def _get_fields(self, table_name, **kwargs):
         raise NotImplementedError()
 
-    def _get_indexes(self, schema, **kwargs):
+    async def _get_indexes(self, schema, **kwargs):
         raise NotImplementedError()
 
-    def _set_index(self, schema, name, field_names, **kwargs):
+    async def _set_index(self, schema, name, field_names, **kwargs):
         raise NotImplementedError()
 
-    def _insert(self, schema, fields, **kwargs):
+    async def _insert(self, schema, fields, **kwargs):
         raise NotImplementedError()
 
-    def _inserts(self, schema, field_names, field_values, **kwargs):
+    async def _inserts(self, schema, field_names, field_values, **kwargs):
         raise NotImplementedError()
 
-    def _update(self, schema, fields, query, **kwargs):
+    async def _update(self, schema, fields, query, **kwargs):
         raise NotImplementedError()
 
-    def _upsert(self, schema, insert_fields, update_fields, conflict_field_names, **kwargs):
+    async def _upsert(
+        self,
+        schema,
+        insert_fields,
+        update_fields,
+        conflict_field_names,
+        **kwargs
+    ):
         raise NotImplementedError()
 
-    def _delete(self, schema, query, **kwargs):
+    async def _delete(self, schema, query, **kwargs):
         raise NotImplementedError()
 
-    def _get(self, schema, query, **kwargs):
+    async def _get(self, schema, query, **kwargs):
         raise NotImplementedError()
 
-    def _count(self, schema, query, **kwargs):
+    async def _count(self, schema, query, **kwargs):
         raise NotImplementedError()
 
-    def render(self, schema, query, **kwargs):
-        """Render the query in a way that the interface can interpret it
-
-        so in a SQL interface, this would render SQL, this is mainly used for
-        debugging
-
-        :param query: Query, the Query instance to render
-        :param **kwargs: any named arguments
-        :returns: mixed
-        """
-        raise NotImplementedError()
-
-    def _handle_unique_error(self, e, **kwargs):
+    async def _handle_unique_error(self, e, **kwargs):
         return False
 
-    def _handle_field_error(self, e, **kwargs):
+    async def _handle_field_error(self, e, **kwargs):
         return False
 
-    def _handle_table_error(self, e, **kwargs):
+    async def _handle_table_error(self, e, **kwargs):
         return False
 
-    def _handle_general_error(self, e, **kwargs):
+    async def _handle_general_error(self, e, **kwargs):
         return False
 
-    def _handle_close_error(self, e, **kwargs):
+    async def _handle_close_error(self, e, **kwargs):
         self.close()
         return True
 
+    def render(self, schema, query, **kwargs):
+        """Render the query in a way that the interface can interpret it
+
+        so in a SQL interface, this would render SQL, this is mainly used for
+        debugging
+
+        :param query: Query, the Query instance to render
+        :param **kwargs: any named arguments
+        :returns: Any
+        """
+        raise NotImplementedError()
+
 
 class Interface(InterfaceABC):
 
@@ -321,40 +351,56 @@ class Interface(InterfaceABC):
     config = None
     """a config.Connection() instance"""
 
-    InterfaceError = InterfaceError
-    UniqueError = UniqueError
-    TableError = TableError
-    FieldError = FieldError
-    UniqueError = UniqueError
-    CloseError = CloseError
-
-    @classmethod
-    def configure(cls, config):
-        host = config.host
-        if host:
-            db = config.database
-            config.database = db.strip("/")
-        return config
-
     def __init__(self, config=None):
         self.config = config
 
-        # enables cleanup of open sockets even if the object isn't correctly garbage collected
-        weakref.finalize(self, self.__del__)
-
-    def __del__(self):
-        """Whenever this gets garbage collected close everything. This is also the
-        method for weakref.finalize"""
-        self.close()
-
-    def connect(self, config=None, *args, **kwargs):
-        """
-        connect to the interface
-
-        this will set the raw db connection to self.connection
-
-        *args -- anything you want that will help the db connect
-        **kwargs -- anything you want that the backend db connection will need to actually connect
+    async def connect(self, config=None, *args, **kwargs):
+        """connect to the interface
+
+        The parent Interface (this class) never interacts with a connection
+        directly, it uses the connection interface for all use/manipulation
+        of the connection:
+
+            * connect: This handles making the actual connection
+            * configure_connection: This is called immediately after a 
+                connection is ready and can be used to further configure the
+                connection, if needed. This is *NEVER* called by this class, so
+                it is up to the child interface to decide when a connection is
+                ready and to call this method
+            * close: This handles closing the actual connection
+            * get_connection: Whenever a connection is needed this class will
+                use the .connection context manager which calls this method to
+                get an actual connection, this should return a connection
+                instance
+            * free_connection: Whenever this class is done with a connection
+                this method will be called, usually at the end of the
+                .connection context manager call
+            * readonly: Responsible for setting a connection to readonly, this
+                is called in .configure_connection but can be called separately
+                also
+            * is_connected: Returns True if an active connection has been made
+            * reconnect: Close any open connections and then call .connect()
+                again
+            * connection: The connection context manager that calls
+                .get_connection and .free_connection, most interactions with
+                a connection will use this
+            * transaction: wraps the .connection context manager and starts a
+                transaction
+
+        The reason why this class uses the connection interface and that no
+        connection anything is fleshed out is because that allows the child
+        interfaces a lot of flexibility in how they handle the connection, they
+        can have a single connection or a pool of connections, etc.
+
+        The amount of methods that will need to be actually implemented by the
+        child interfaces is up to the child interfaces themselves
+
+        :param config: Config, this doesn't need to be passed in because it can
+            also be passed into the __init__ method, a valid Config object needs
+            to be present before a connection can be made though
+        :param *args: anything you want that will help the db connect
+        :param **kwargs: anything you want that the backend db connection will
+            need to actually connect
         """
         if self.connected:
             return self.connected
@@ -362,176 +408,235 @@ def connect(self, config=None, *args, **kwargs):
         if config:
             self.config = config
 
+        await self.configure(self.config)
+
         try:
-            self._connect(self.config)
+            await self._connect(self.config)
             self.connected = True
 
         except Exception as e:
             self.connected = False
-            self.raise_error(e)
+            await self.raise_error(e)
 
-        self.log_debug(f"Connected {self.config.interface_name}")
-        self.configure_connection()
+        self.log_debug("Connected {}", self.config.interface_name)
         return self.connected
 
-    def reconnect(self):
-        self.close()
-        self.connect()
+    async def reconnect(self):
+        await self.close()
+        await self.connect()
+
+    async def configure_connection(self, **kwargs):
+        """Configure a ready connection
+
+        It is up to the child interfaces to decide when to call this method
 
-    def configure_connection(self, **kwargs):
-        kwargs.setdefault("prefix", "configure_connection")
-        self.execute(
+        :param **kwargs:
+            - connection: Any, the connection to configure
+        """
+        await self.execute(
             self._configure_connection,
             **kwargs
         )
 
-    def get_connection(self):
-        """Any time you need a connection it should be retrieved through .connection,
-        and that method uses this method
+        self.config.readonly = kwargs.get("readonly", self.config.readonly)
+
+        # by default we can read/write, so only bother to run this if we
+        # need to actually make the connection readonly
+        if self.config.readonly:
+            await self.readonly(
+                self.config.readonly,
+                **kwargs
+            )
+
+    async def get_connection(self):
+        """Any time you need a connection it should be retrieved through
+        .connection, and that method uses this method
 
         :returns: Connection instance
         """
         if not self.is_connected():
-            self.connect()
+            await self.connect()
 
-        connection = self._get_connection()
+        connection = await self._get_connection()
 
         if connection.closed:
             # we've gotten into a bad state so let's try reconnecting
-            self.reconnect()
-            connection = self._get_connection()
+            await self.reconnect()
+            connection = await self._get_connection()
 
         connection.interface = self
 
         self.log_debug(
-            f"Getting {self.config.interface_name} connection {connection}"
+            "Getting {} connection {}",
+            self.config.interface_name,
+            connection
         )
         return connection
 
-    def free_connection(self, connection):
+    async def free_connection(self, connection):
         """When .connection is done with a connection it calls this method"""
-        #connection.interface = None
         if self.is_connected():
             self.log_debug(
-                f"Freeing {self.config.interface_name} connection {connection}"
+                "Freeing {} connection {}",
+                self.config.interface_name,
+                connection
             )
-            self._free_connection(connection)
+            await self._free_connection(connection)
 
     def is_connected(self):
+        """Returns True if this Interface has been connected"""
         return self.connected
 
-    def close(self):
+    async def close(self):
         """close an open connection"""
-        if not self.connected: return True
+        if not self.connected:
+            return True
 
-        self._close()
+        await self._close()
         self.connected = False
-        self.log_debug(f"Closed Connection {self.config.interface_name}")
+        self.log_debug("Closed Connection {}", self.config.interface_name)
         return True
 
-    def readonly(self, readonly=True, **kwargs):
-        """Make the connection read only (pass in True) or read/write (pass in False)
-
-        :param readonly: boolean, True if this connection should be readonly, False
-            if the connection should be read/write
-        """
-        self.log_warning([
-            f"Setting interface {self.config.interface_name}",
-            f"to readonly={readonly}",
-        ])
-        self.config.readonly = readonly
-
-        if self.connected:
-            kwargs.setdefault("prefix", "readonly")
-            self.execute(
-                self._readonly,
-                readonly,
-                **kwargs
-            )
-            #self._readonly(readonly, **kwargs)
-
-    @contextmanager
-    def connection(self, connection=None, **kwargs):
-        """Any time you need a connection you should use this context manager, this
-        is the only place that wraps exceptions in InterfaceError, so all connections
-        should go through this method or .transaction if you need to start a transaction
+    @asynccontextmanager
+    async def connection(self, connection=None, **kwargs):
+        """Any time you need a connection you should use this context manager,
+        this is the only place that wraps exceptions in InterfaceError, so all
+        connections should go through this method or .transaction if you need
+        to start a transaction
 
         :Example:
             with self.connection(**kwargs) as connection:
                 # do something with connection
         """
         free_connection = False
+
+        prefix = kwargs.get("prefix", "")
+        if prefix:
+            prefix += " "
+
         try:
             if connection:
                 if connection.closed:
                     self.log_warning(
-                        "Passed in connection is closed and must be refreshed"
+                        "{}Existing connection {} is closed",
+                        prefix,
+                        connection
                     )
-                    if connection.transaction_exists():
-                        self.log_warning("Closed connection had open transactions!")
 
                     connection = None
 
                 else:
                     self.log_debug(
-                        f"Connection call using existing connection {connection}"
+                        "{}Using existing connection {}",
+                        prefix,
+                        connection
                     )
-                    yield connection
 
             if connection is None:
                 free_connection = True
-                connection = self.get_connection()
-                yield connection
+
+                self.log_debug(
+                    "{}Getting connection",
+                    prefix,
+                )
+
+                connection = await self.get_connection()
+
+                self.log_debug(
+                    "{}Got connection {}",
+                    prefix,
+                    connection
+                )
+
+            yield connection
 
         except Exception as e:
-            self.raise_error(e)
+            await self.raise_error(e)
 
         finally:
             if free_connection and connection:
-                self.free_connection(connection)
+                await self.free_connection(connection)
+
+                self.log_debug(
+                    "{}Connection {} was freed",
+                    prefix,
+                    connection
+                )
 
             else:
                 self.log_debug(
-                    f"Connection call NOT freeing existing connection {connection}"
+                    "{}Existing connection {} was NOT freed",
+                    prefix,
+                    connection
                 )
 
-    @contextmanager
-    def transaction(self, connection=None, **kwargs):
+    @asynccontextmanager
+    async def transaction(self, connection=None, **kwargs):
         """A simple context manager useful for when you want to wrap a bunch of
-        db calls in a transaction, this is used internally for any write statements
+        db calls in a transaction, this is used internally for any write
+        statements
+
+        NOTE -- psycopg3 now has a transaction context manager:
+            https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.Connection.transaction
+
+            It might be worth switching over to it sometime in the future? I'm
+            not sure how that would work with sqlite though
 
         :Example:
             with self.transaction() as connection
                 # do a bunch of calls
             # those db calls will be committed by this line
         """
-        with self.connection(connection) as connection:
-            connection.transaction_start(**kwargs)
+        async with self.connection(connection, **kwargs) as connection:
+            await connection.transaction_start(**kwargs)
             try:
                 yield connection
 
             except Exception:
-                connection.transaction_fail()
+                await connection.transaction_fail()
                 raise
 
             else:
-                connection.transaction_stop()
+                await connection.transaction_stop()
 
-    def execute_write(self, callback, *args, **kwargs):
-        """Any write statements will use this method
+    async def readonly(self, readonly=True, **kwargs):
+        """Make the connection read only (pass in True) or read/write (pass in
+        False)
 
-        CREATE, DELETE, DROP, INSERT, or UPDATE (collectively "write statements")
+        :param readonly: boolean, True if this connection should be readonly,
+            False if the connection should be read/write
         """
-        kwargs.setdefault("execute_in_transaction", True)
+        self.log_warning([
+            f"Setting interface {self.config.interface_name}",
+            f"to readonly={readonly}",
+        ])
+        self.config.readonly = readonly
 
-        return self.execute(callback, *args, **kwargs)
+        await self.execute(
+            self._readonly,
+            readonly,
+            **kwargs
+        )
 
-    def execute_read(self, callback, *args, **kwargs):
+    async def execute_write(self, callback, *args, **kwargs):
         """Any write statements will use this method
 
-        SELECT (collectively "read statements")
+        collectively "write statements":
+            CREATE, DELETE, DROP, INSERT, or UPDATE
         """
-        with self.connection(**kwargs) as connection:
+        kwargs.setdefault("execute_in_transaction", True)
+        return await self.execute(callback, *args, **kwargs)
+
+    async def execute_read(self, callback, *args, **kwargs):
+        """Any read statements will use this method
+
+        collectively "read statements":
+            SELECT
+        """
+        kwargs["prefix"] = "execute_read"
+        # we need the connection so we can decide if we need to run the query
+        # in a tx or not
+        async with self.connection(**kwargs) as connection:
             kwargs["connection"] = connection
 
             kwargs.setdefault(
@@ -539,27 +644,34 @@ def execute_read(self, callback, *args, **kwargs):
                 connection.transaction_exists()
             )
 
-            return self.execute(callback, *args, **kwargs)
+            return await self.execute(callback, *args, **kwargs)
 
-    def execute(self, callback, *args, **kwargs):
+    async def execute(self, callback, *args, **kwargs):
         """Internal method. Execute the callback with args and kwargs, retrying
         the query if an error is raised that it thinks it successfully handled
 
-        This is called by .execute_write, .execute_read, and .query
+        This is called by .execute_write, .execute_read, and .raw
 
         :param callback: callable, this will be run at-most twice
         :param *args: passed directly to callback as *args
         :param **kwargs: passed to callback as **kwargs, can have values added
-        :returns: mixed, whatever the callback returns
+        :returns: Any, whatever the callback returns
         """
-        prefix = kwargs.pop("prefix", callback.__name__)
+        # we want to override the prefix at this point
+        kwargs.pop("prefix", None)
+        prefix = callback.__name__
 
         try:
-            return self._execute(callback, *args, prefix=prefix, **kwargs)
+            return await self._execute(
+                callback,
+                *args,
+                prefix=prefix,
+                **kwargs
+            )
 
         except Exception as e:
-            if self.handle_error(e=e, prefix=prefix, **kwargs):
-                return self._execute(
+            if await self.handle_error(e=e, prefix=prefix, **kwargs):
+                return await self._execute(
                     callback,
                     *args,
                     prefix=f"{prefix}_retry",
@@ -567,102 +679,129 @@ def execute(self, callback, *args, **kwargs):
                 )
 
             else:
-                self.raise_error(e)
+                await self.raise_error(e)
 
-    def _execute(self, callback, *args, **kwargs):
-        """Internal method. Called by .execute"""
-        in_transaction = kwargs.get("execute_in_transaction", False)
+    async def _execute(self, callback, *args, **kwargs):
+        """Internal method for .execute, this should never be called directly,
+        it's broken out from .execute so .execute can re-run it if an error is
+        encountered. Because the transaction is started in this method then
+        it will be rolledback before .execute starts error handling, which is
+        what we want.
 
-        if in_transaction:
-            with self.transaction(**kwargs) as connection:
-                kwargs["connection"] = connection
-                return callback(*args, **kwargs)
+        :param callback: callable, this will be run at-most twice
+        :param *args: passed directly to callback as *args
+        :param **kwargs: passed to callback as **kwargs, can have values added
+            - execute_only: bool, don't wrap the callback call in a transaction
+                or connection context manager
+            - execute_in_transaction: bool, True if callback should be wrapped
+                in a tx, only applies if execute_only=False
+        :returns: Any, whatever the callback returns
+        """
+        execute_only = kwargs.get("execute_only", False)
+
+        if execute_only:
+            return await callback(*args, **kwargs)
 
         else:
-            with self.connection(**kwargs) as connection:
-                kwargs["connection"] = connection
-                return callback(*args, **kwargs)
+            in_transaction = kwargs.get("execute_in_transaction", False)
 
-    def has_table(self, table_name, **kwargs):
-        """
-        check to see if a table is in the db
+            if in_transaction:
+                async with self.transaction(**kwargs) as connection:
+                    kwargs["connection"] = connection
+                    return await callback(*args, **kwargs)
+
+            else:
+                async with self.connection(**kwargs) as connection:
+                    kwargs["connection"] = connection
+                    return await callback(*args, **kwargs)
 
-        table_name -- string -- the table to check
-        return -- boolean -- True if the table exists, false otherwise
+    async def has_table(self, table_name, **kwargs):
+        """Check to see if a table is in the db
+
+        :param table_name: str, the table to check
+        :returns: bool, True if the table exists, false otherwise
         """
-        kwargs.setdefault("prefix", "has_table")
-        tables = self.execute_read(
-            self.get_tables,
+        tables = await self.execute_read(
+            self._get_tables,
             table_name,
             **kwargs
         )
         return len(tables) > 0
 
-    def get_tables(self, table_name="", **kwargs):
-        """
-        get all the tables of the currently connected db
+    async def get_tables(self, table_name="", **kwargs):
+        """Get all the tables of the currently connected db
 
-        table_name -- string -- if you would like to filter the tables list to only include matches with this name
-        return -- list -- a list of table names
+        :param table_name: str, if you would like to filter the tables list to
+            only include matches with this name
+        :returns: list, a list of table names
         """
-        kwargs.setdefault("prefix", "get_tables")
-        return self.execute_read(
+        return await self.execute_read(
             self._get_tables,
             str(table_name),
             **kwargs
         )
 
-    def set_table(self, schema, **kwargs):
+    async def set_table(self, schema, **kwargs):
         """
         add the table to the db
 
-        :param schema: Schema instance, contains all the information about the table
+        :param schema: Schema instance, contains all the information about the
+            table
         """
-        kwargs.setdefault("prefix", "set_table")
-        with self.transaction(**kwargs) as connection:
-            kwargs['connection'] = connection
+        kwargs["prefix"] = "set_table"
+        async with self.transaction(**kwargs) as connection:
+            kwargs["connection"] = connection
 
-            self._set_table(schema=schema, **kwargs)
+            await self.execute_write(
+                self._set_table,
+                schema=schema,
+                #connection=connection,
+                #execute_only=True,
+                **kwargs
+            )
 
             for index_name, index in schema.indexes.items():
-                self._set_index(
+                await self.execute_write(
+                    self._set_index,
                     schema=schema,
                     name=index.name,
                     field_names=index.field_names,
-                    connection=connection,
+                    #connection=connection,
+                    #execute_only=True
                     **index.options,
                 )
 
-    def unsafe_delete_table(self, schema, **kwargs):
-        """wrapper around delete_table that matches the *_tables variant and denotes
-        that this is a serious operation
+    async def unsafe_delete_table(self, schema, **kwargs):
+        """wrapper around delete_table that matches the *_tables variant and
+        denotes that this is a serious operation
 
         remove a table matching schema from the db
 
         :param schema: Schema instance, the table to delete
         """
-        kwargs.setdefault("prefix", "unsafe_delete_table")
-        self.execute_write(
+        await self.execute_write(
             self._delete_table,
             schema=schema,
             **kwargs
         )
         return True
 
-    def unsafe_delete_tables(self, **kwargs):
+    async def unsafe_delete_tables(self, **kwargs):
         """Removes all the tables from the db
 
         https://github.com/Jaymon/prom/issues/75
         """
-        kwargs.setdefault("prefix", "unsafe_delete_tables")
-        with self.transaction(**kwargs) as connection:
+        kwargs["prefix"] = "unsafe_delete_tables"
+        async with self.transaction(**kwargs) as connection:
             kwargs['connection'] = connection
             kwargs.setdefault('nest', False)
-            for table_name in self.get_tables(**kwargs):
-                self._delete_table(table_name, **kwargs)
+            for table_name in await self.get_tables(**kwargs):
+                # we don't wrap this in an .execute_write because there isn't
+                # anything to recover from if it fails
+                await self._delete_table(table_name, **kwargs)
         return True
 
-    def get_indexes(self, schema, **kwargs):
+    async def get_indexes(self, schema, **kwargs):
         """
         get all the indexes
 
@@ -670,24 +809,23 @@ def get_indexes(self, schema, **kwargs):
 
         return -- dict -- the indexes in {indexname: fields} format
         """
-        kwargs.setdefault("prefix", "get_indexes")
-        return self.execute_read(
+        return await self.execute_read(
             self._get_indexes,
             schema=schema,
             **kwargs
         )
 
-    def set_index(self, schema, name, field_names, **kwargs):
+    async def set_index(self, schema, name, field_names, **kwargs):
         """
         add an index to the table
 
-        schema -- Schema()
-        name -- string -- the name of the index
-        field_names -- array -- the fields the index should be on
-        **index_options -- dict -- any index options that might be useful to create the index
+        :param schema: Schema
+        :param name: str, the name of the index
+        :param field_names: list, the fields the index should be on
+        :param **index_options: any index options that might be useful to
+            create the index
         """
-        kwargs.setdefault("prefix", "set_index")
-        self.execute_write(
+        await self.execute_write(
             self._set_index,
             schema=schema,
             name=name,
@@ -696,7 +834,7 @@ def set_index(self, schema, name, field_names, **kwargs):
         )
         return True
 
-    def insert(self, schema, fields, **kwargs):
+    async def insert(self, schema, fields, **kwargs):
         """Persist fields into the db
 
         :param schema: Schema instance, the table the query will run against
@@ -704,29 +842,27 @@ def insert(self, schema, fields, **kwargs):
         :param **kwargs: passed through
         :returns: mixed, will return the primary key values
         """
-        kwargs.setdefault("prefix", "insert")
-        return self.execute_write(
+        return await self.execute_write(
             self._insert,
             schema=schema,
             fields=fields,
             **kwargs
         )
 
-    def inserts(self, schema, field_names, field_rows, **kwargs):
+    async def inserts(self, schema, field_names, field_rows, **kwargs):
         """Persist the field names found in all the field rows
 
         :param schema: Schema
         :param field_names: list, the field names that will be checked for each
-            row in field_rows and used to turn each dict in field_rows to a tuple
-        :param field_rows: Sequence, if an iterator of dict instances with keys found
-            in field_names, if a key is missing it will have None set as the value,
-            if it's an iterator of values then the tuple value ordering should match
-            with field_names
+            row in field_rows and used to turn each dict in field_rows to a
+            tuple
+        :param field_rows: Sequence, if an iterator of dict instances with keys
+            found in field_names, if a key is missing it will have None set as
+            the value, if it's an iterator of values then the tuple value
+            ordering should match with field_names
         :param **kwargs: passed through
         :returns: bool, True if the query executed successfully
         """
-        kwargs.setdefault("prefix", "inserts")
-
         def field_values(field_names, field_rows):
             for fields in field_rows:
                 if isinstance(fields, Mapping):
@@ -738,7 +874,7 @@ def field_values(field_names, field_rows):
                 else:
                     yield fields
 
-        self.execute_write(
+        await self.execute_write(
             self._inserts,
             schema=schema,
             field_names=field_names,
@@ -747,7 +883,7 @@ def field_values(field_names, field_rows):
         )
         return True
 
-    def update(self, schema, fields, query, **kwargs):
+    async def update(self, schema, fields, query, **kwargs):
         """Persist the query.fields into the db that match query.fields_where
 
         :param schema: Schema instance, the table the query will run against
@@ -756,8 +892,7 @@ def update(self, schema, fields, query, **kwargs):
         :param **kwargs: passed through
         :returns: int, how many rows where updated
         """
-        kwargs.setdefault("prefix", "update")
-        return self.execute_write(
+        return await self.execute_write(
             self._update,
             schema=schema,
             fields=fields,
@@ -765,7 +900,14 @@ def update(self, schema, fields, query, **kwargs):
             **kwargs,
         )
 
-    def upsert(self, schema, insert_fields, update_fields, conflict_field_names, **kwargs):
+    async def upsert(
+        self,
+        schema,
+        insert_fields,
+        update_fields,
+        conflict_field_names,
+        **kwargs
+    ):
         """Perform an upsert (insert or update) on the table
 
         :param schema: Schema instance, the table the query will run against
@@ -775,10 +917,9 @@ def upsert(self, schema, insert_fields, update_fields, conflict_field_names, **k
         :param conflict_field_names: list, the field names that will decide if
             an insert or update is performed
         :param **kwargs: anything else
-        :returns: mixed, the primary key
+        :returns: str|int|None, the primary key
         """
-        kwargs.setdefault("prefix", "upsert")
-        return self.execute_write(
+        return await self.execute_write(
             self._upsert,
             schema=schema,
             insert_fields=insert_fields,
@@ -787,67 +928,78 @@ def upsert(self, schema, insert_fields, update_fields, conflict_field_names, **k
             **kwargs,
         )
 
-    def delete(self, schema, query, **kwargs):
+    async def delete(self, schema, query, **kwargs):
         """delete matching rows according to query filter criteria
 
         :param schema: Schema instance, the table the query will run against
-        :param query: Query instance, the filter criteria, this will fail if empty
+        :param query: Query instance, the filter criteria, this will fail if
+            empty
         :returns: int, how many rows were deleted ... I think
         """
         if not query or not query.fields_where:
-            raise ValueError('aborting delete because there is no where clause')
+            raise ValueError("aborting delete because there is no where clause")
+
+        return await self.unsafe_delete(schema, query, **kwargs)
+
+    async def unsafe_delete(self, schema, query, **kwargs):
+        """delete matching rows
 
-        kwargs.setdefault("prefix", "delete")
-        return self.execute_write(
+        WARNING -- this can clear the whole table, you should mainly use
+            .delete and always include filtering criteria in the query
+
+        :param schema: Schema instance, the table the query will run against
+        :param query: Query instance, the filter criteria, this will fail if
+            empty
+        :returns: int, how many rows were deleted ... I think
+        """
+        return await self.execute_write(
             self._delete,
             schema=schema,
             query=query,
             **kwargs
         )
 
-    def raw(self, query_str, *query_args, **kwargs):
+    async def raw(self, query_str, *query_args, **kwargs):
         """
         run a raw query on the db
 
-        query_str -- string -- the query to run
-        *query_args -- if the query_str is a formatting string, pass the values in this
-        **kwargs -- any query options can be passed in by using key=val syntax
+        :param query_str: str, the query to run
+        :param *query_args: if the query_str is a formatting string, pass the
+            values in this
+        :param **kwargs: any query options can be passed in by using key=val
+            syntax
         """
-        kwargs.setdefault("prefix", "raw")
-        return self.execute(
+        return await self.execute(
             self._raw,
             query_str,
             *query_args,
             **kwargs
         )
 
-    def get_fields(self, table_name, **kwargs):
-        kwargs.setdefault("prefix", "get_fields")
-        return self.execute_read(
+    async def get_fields(self, table_name, **kwargs):
+        return await self.execute_read(
             self._get_fields,
             str(table_name),
             **kwargs
         )
 
-    def get_one(self, schema, query=None, **kwargs):
+    async def one(self, schema, query=None, **kwargs):
         """get one row from the db matching filters set in query
 
         :param schema: Schema instance, the table the query will run against
         :param query: Query instance, the filter criteria
         :return: dict, the matching row
         """
-        kwargs.setdefault("prefix", "get_one")
-        return self.get(schema, query, fetchone=True, **kwargs) or {}
+        return await self.get(schema, query, fetchone=True, **kwargs) or {}
 
-    def get(self, schema, query=None, **kwargs):
+    async def get(self, schema, query=None, **kwargs):
         """get matching rows from the db matching filters set in query
 
         :param schema: Schema instance, the table the query will run against
         :param query: Query instance, the filter criteria
         :returns: list, a list of matching dicts
         """
-        kwargs.setdefault("prefix", "get")
-        ret = self.execute_read(
+        ret = await self.execute_read(
             self._get,
             schema=schema,
             query=query or Query(),
@@ -855,15 +1007,14 @@ def get(self, schema, query=None, **kwargs):
         )
         return ret or []
 
-    def count(self, schema, query=None, **kwargs):
+    async def count(self, schema, query=None, **kwargs):
         """count matching rows according to query filter criteria
 
         :param schema: Schema instance, the table the query will run against
         :param query: Query instance, the filter criteria
         :returns: list, a list of matching dicts
         """
-        kwargs.setdefault("prefix", "count")
-        ret = self.execute_read(
+        ret = await self.execute_read(
             self._count,
             schema=schema,
             query=query or Query(),
@@ -871,7 +1022,7 @@ def count(self, schema, query=None, **kwargs):
         )
         return int(ret) if ret else 0
 
-    def handle_error(self, e, **kwargs):
+    async def handle_error(self, e, **kwargs):
         """Try and handle the error, return False if the error can't be handled
 
         :param e: Exception, the caught exception
@@ -888,27 +1039,28 @@ def handle_error(self, e, **kwargs):
 
         if isinstance(e, CloseError):
             self.log_debug("Handling a close error")
-            ret = self._handle_close_error(e=e, **kwargs)
+            ret = await self._handle_close_error(e=e, **kwargs)
 
         else:
-            with self.transaction(**kwargs) as connection:
+            kwargs["prefix"] = f"{prefix}_handle_error"
+            async with self.transaction(**kwargs) as connection:
                 kwargs["connection"] = connection
 
                 if isinstance(e, UniqueError):
                     self.log_debug("Handling a unique error")
-                    ret = self._handle_unique_error(e=e, **kwargs)
+                    ret = await self._handle_unique_error(e=e, **kwargs)
 
                 elif isinstance(e, FieldError):
                     self.log_debug("Handling a field error")
-                    ret = self._handle_field_error(e=e, **kwargs)
+                    ret = await self._handle_field_error(e=e, **kwargs)
 
                 elif isinstance(e, TableError):
                     self.log_debug("Handling a table error")
-                    ret = self._handle_table_error(e=e, **kwargs)
+                    ret = await self._handle_table_error(e=e, **kwargs)
 
                 else:
                     self.log_debug("Handling a general error")
-                    ret = self._handle_general_error(e=e, **kwargs)
+                    ret = await self._handle_general_error(e=e, **kwargs)
 
         if ret:
             self.log_info(["Successfully handled", prefix, "error"])
@@ -921,8 +1073,8 @@ def handle_error(self, e, **kwargs):
     def raise_error(self, e, **kwargs):
         """raises e
 
-        :param e: Exception, if a built-in exception then it's raised, if any other
-            error then it will be wrapped in an InterfaceError
+        :param e: Exception, if a built-in exception then it's raised, if any
+            other error then it will be wrapped in an InterfaceError
         """
         e2 = self.create_error(e, **kwargs)
         if e2 is not e:
@@ -931,8 +1083,8 @@ def raise_error(self, e, **kwargs):
             raise e
 
     def create_error(self, e, **kwargs):
-        """create the error that you want to raise, this gives you an opportunity
-        to customize the error
+        """create the error that you want to raise, this gives you an
+        opportunity to customize the error
 
         allow python's built in errors to filter up through
         https://docs.python.org/2/library/exceptions.html
@@ -948,7 +1100,8 @@ def create_error(self, e, **kwargs):
         if not isinstance(e, error_class):
             if not hasattr(builtins, e.__class__.__name__):
                 if "error_module" in kwargs:
-                    if kwargs["error_module"].__name__ in e.__class__.__module__:
+                    errmod_name = kwargs["error_module"].__name__
+                    if errmod_name in e.__class__.__module__:
                         e = error_class(e)
 
                 else:
diff --git a/prom/interface/postgres.py b/prom/interface/postgres.py
new file mode 100644
index 0000000..89e15ef
--- /dev/null
+++ b/prom/interface/postgres.py
@@ -0,0 +1,507 @@
+# -*- coding: utf-8 -*-
+import re
+import os
+import sys
+import decimal
+import datetime
+import uuid
+import json
+from collections import Counter
+
+# third party
+import psycopg
+from psycopg.adapt import Dumper
+
+# first party
+from .sql import SQLInterface, SQLConnection
+from ..compat import *
+from ..utils import get_objects
+from ..exception import (
+    InterfaceError,
+    UniqueError,
+    TableError,
+    FieldError,
+    UniqueError,
+    CloseError,
+    PlaceholderError,
+)
+
+
+class DictDumper(Dumper):
+    """Converts from python dict to JSONB to be saved into the db
+
+    https://www.psycopg.org/psycopg3/docs/advanced/adapt.html#adaptation
+    https://www.psycopg.org/psycopg3/docs/api/adapt.html#psycopg.adapt.Dumper
+
+    Surprisingly, Postgres converts the json value in the db back to dict just
+    fine but does not adapt a dict, I'm not sure why
+    """
+    TYPE = dict
+
+    def dump(self, val):
+        """adapter should be a function taking a single argument (the object to
+        adapt) and returning an object conforming to the ISQLQuote protocol
+        (e.g. exposing a getquoted() method). Once an object is registered, it
+        can be safely used in SQL queries and by the adapt() function
+
+        :param val: dict, the value coming from Python destined for Postgres
+        :returns: bytes
+        """
+        return ByteString(json.dumps(val))
+
+
+class AsyncPostgreSQLConnection(SQLConnection, psycopg.AsyncConnection):
+    """
+    https://www.psycopg.org/docs/connection.html
+    """
+    pass
+
+
+class PostgreSQL(SQLInterface):
+    """
+    https://www.psycopg.org/psycopg3/docs/advanced/async.html
+    https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html
+
+    https://www.psycopg.org/docs/
+    https://www.psycopg.org/docs/usage.html
+    """
+    LIMIT_NONE = "ALL"
+
+    _connection = None
+
+    @classmethod
+    async def configure(cls, config):
+        port = config.port
+        if not port:
+            config.port = 5432
+        return config
+
+    def get_paramstyle(self):
+        """
+        https://www.psycopg.org/docs/module.html#psycopg2.paramstyle
+        """
+        return psycopg.paramstyle
+
+    async def _connect(self, config):
+        """
+        https://www.psycopg.org/psycopg3/docs/api/connections.html
+
+        If I ever wanted to add pool support back:
+            https://www.psycopg.org/psycopg3/docs/advanced/pool.html
+        """
+        self._connection = await AsyncPostgreSQLConnection.connect(
+            dbname=config.database,
+            user=config.username,
+            password=config.password,
+            host=config.host,
+            port=config.port,
+            row_factory=psycopg.rows.dict_row,
+            autocommit=True, # if False there will be random hangs
+        )
+
+        self.log_debug("Connected to connection {}", self._connection)
+
+        await self.configure_connection(connection=self._connection)
+
+    async def _configure_connection(self, **kwargs):
+        kwargs["prefix"] = "_configure_connection"
+        async with self.connection(**kwargs) as connection:
+            connection.adapters.register_dumper(DictDumper.TYPE, DictDumper)
+
+    async def _get_connection(self):
+        return self._connection
+
+    async def _close(self):
+        await self._connection.close()
+        self._connection = None
+
+    async def _readonly(self, readonly, **kwargs):
+        """
+        https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.Connection.set_read_only
+        https://www.psycopg.org/psycopg3/docs/api/connections.html#psycopg.AsyncConnection.set_read_only
+        """
+        async with self.connection(**kwargs) as connection:
+            kwargs["connection"] = connection
+
+            await connection.set_read_only(readonly)
+
+            # https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html - search
+            # for "read_only"
+            # https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-DEFAULT-TRANSACTION-READ-ONLY
+            await self._raw(
+                'SET default_transaction_read_only TO {}'.format(
+                    "true" if readonly else "false"),
+                ignore_result=True,
+                **kwargs
+            )
+
+    async def _get_tables(self, table_name, **kwargs):
+        query_str = "\n".join([
+            "SELECT",
+            "  tablename",
+            "FROM",
+            "  pg_tables",
+            "WHERE",
+            "  tableowner = {}".format(self.PLACEHOLDER)
+        ])
+        query_args = [self.config.username]
+
+        if table_name:
+            query_str += " AND tablename = {}".format(self.PLACEHOLDER)
+            query_args.append(str(table_name))
+
+        schema_name = kwargs.get("schema_name", "public")
+        if schema_name:
+            query_str += " AND schemaname = {}".format(self.PLACEHOLDER)
+            query_args.append(schema_name)
+
+        ret = await self._raw(query_str, *query_args, **kwargs)
+        # http://www.postgresql.org/message-id/CA+mi_8Y6UXtAmYKKBZAHBoY7F6giuT5WfE0wi3hR44XXYDsXzg@mail.gmail.com
+        return [r['tablename'] for r in ret]
+
+    async def _delete_table(self, schema, **kwargs):
+        """
+        https://www.postgresql.org/docs/current/sql-droptable.html
+        """
+        query_str = "DROP TABLE IF EXISTS {} CASCADE".format(
+            self.render_table_name_sql(schema)
+        )
+        await self._raw(query_str, ignore_result=True, **kwargs)
+
+    async def _get_indexes(self, schema, **kwargs):
+        """return all the indexes for the given schema"""
+        ret = {}
+        query_str = [
+            "SELECT",
+            "  tbl.relname AS table_name,",
+            "  i.relname AS index_name,",
+            "  a.attname AS field_name,",
+            "  ix.indkey AS index_order,",
+            "  a.attnum AS field_num",
+            "FROM",
+            "  pg_class tbl, pg_class i, pg_index ix, pg_attribute a",
+            "WHERE",
+            "  tbl.oid = ix.indrelid",
+            "  AND i.oid = ix.indexrelid",
+            "  AND a.attrelid = tbl.oid",
+            "  AND a.attnum = ANY(ix.indkey)",
+            "  AND tbl.relkind = {}".format(self.PLACEHOLDER),
+            "  AND tbl.relname = {}".format(self.PLACEHOLDER),
+            "ORDER BY",
+            "  tbl.relname, i.relname",
+        ]
+        query_str = "\n".join(query_str)
+
+        kwargs["cursor_result"] = True
+        indexes = await self._raw(query_str, "r", str(schema), **kwargs)
+
+        # massage the data into more readable {index_name: fields} format
+        async for idict in indexes:
+            if idict['index_name'] not in ret:
+                ret[idict['index_name']] = list(
+                    map(int, idict['index_order'].split(' '))
+                )
+
+            i = ret[idict['index_name']].index(idict['field_num'])
+            ret[idict['index_name']][i] = idict['field_name']
+
+        return ret
+
+    async def _get_fields(self, table_name, **kwargs):
+        """return all the fields for the given schema"""
+        ret = {}
+
+        # I had to brush up on my join knowledge while writing this query
+        # https://en.wikipedia.org/wiki/Join_(SQL)
+        #
+        # other helpful links
+        # * https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns
+        # * https://www.postgresql.org/docs/9.4/static/catalog-pg-attribute.html
+        # * https://www.postgresql.org/docs/9.3/static/catalog-pg-type.html
+        # 
+        # another approach
+        # * http://dba.stackexchange.com/questions/22362/
+        # * http://gis.stackexchange.com/questions/94049/
+        query_str = "\n".join([
+            "SELECT",
+            "  a.attnum,",
+            "  a.attname,",
+            "  a.attnotnull,",
+            "  t.typname,",
+            "  i.indisprimary,",
+            "  c.relname AS confrelname",
+            "FROM",
+            "  pg_attribute a",
+            "JOIN pg_type t ON a.atttypid = t.oid",
+            "LEFT JOIN",
+            "  pg_index i ON a.attrelid = i.indrelid",
+            "  AND a.attnum = any(i.indkey)",
+            "LEFT JOIN",
+            "  pg_constraint s ON a.attrelid = s.conrelid",
+            "  AND s.contype = {}".format(self.PLACEHOLDER),
+            "  AND a.attnum = any(s.conkey)",
+            "LEFT JOIN",
+            "  pg_class c ON s.confrelid = c.oid",
+            "WHERE",
+            "  a.attrelid = {}::regclass".format(self.PLACEHOLDER),
+            "  AND a.attisdropped = False",
+            "  AND a.attnum > 0",
+            "ORDER BY a.attnum ASC",
+        ])
+        query_args = ["f", self.render_table_name_sql(table_name)]
+        kwargs["cursor_result"] = True
+        fields = await self._raw(query_str, *query_args, **kwargs)
+
+        pg_types = {
+            "float4": float,
+            "float8": float,
+            "timestamp": datetime.datetime,
+            "timestamptz": datetime.datetime,
+            "int2": int,
+            "int4": int,
+            "int8": long,
+            "numeric": decimal.Decimal,
+            "text": str,
+            "citext": str,
+            "bpchar": str,
+            "varchar": str,
+            "bool": bool,
+            "date": datetime.date,
+            "blob": bytes,
+            "bytea": bytes,
+            "jsonb": bytes,
+            "uuid": uuid.UUID,
+        }
+
+        # the rows we can set:
+        #   field_type, name, field_required, min_size, max_size, size, unique,
+        #   pk, <foreign key info>
+        # These keys will roughly correspond with schema.Field
+        async for row in fields:
+            field = {
+                "name": row["attname"],
+                "field_type": pg_types[row["typname"]],
+                "field_required": row["attnotnull"],
+                "pk": bool(row["indisprimary"]),
+            }
+
+            if row["confrelname"]:
+                # TODO -- I can't decide which name I like
+                field["schema_table_name"] = row["confrelname"]
+                field["ref_table_name"] = row["confrelname"]
+
+            if row["typname"] == "citext":
+                field["ignore_case"] = True
+
+            ret[field["name"]] = field
+
+        return ret
+
+    def render_sort_field_sql(self, field_name, field_vals, sort_dir_str):
+        # this solution is based off:
+        # http://postgresql.1045698.n5.nabble.com/ORDER-BY-FIELD-feature-td1901324.html
+        # see also: https://gist.github.com/cpjolicoeur/3590737
+        query_sort_str = []
+        query_args = []
+        for v in reversed(field_vals):
+            query_sort_str.append('  {} = {} {}'.format(
+                self.render_field_name_sql(field_name),
+                self.PLACEHOLDER,
+                sort_dir_str
+            ))
+            query_args.append(v)
+
+        return ',\n'.join(query_sort_str), query_args
+
+    def render_date_field_sql(self, field_name, field_kwargs, symbol):
+        """
+        allow extracting information from date
+
+        http://www.postgresql.org/docs/8.3/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
+        """
+        fstrs = []
+        k_opts = {
+            'century': 'EXTRACT(CENTURY FROM {})',
+            'day': 'EXTRACT(DAY FROM {})',
+            'decade': 'EXTRACT(DECADE FROM {})',
+            'weekday': 'EXTRACT(DOW FROM {})',
+            'dow': 'EXTRACT(DOW FROM {})',
+            'isodow': 'EXTRACT(ISODOW FROM {})',
+            'epoch': 'EXTRACT(EPOCH FROM {})',
+            'hour': 'EXTRACT(HOUR FROM {})',
+            'year': 'EXTRACT(YEAR FROM {})',
+            'isoyear': 'EXTRACT(ISOYEAR FROM {})',
+            'minute': 'EXTRACT(MINUTE FROM {})',
+            'month': 'EXTRACT(MONTH FROM {})',
+            'quarter': 'EXTRACT(QUARTER FROM {})',
+            'week': 'EXTRACT(WEEK FROM {})',
+        }
+
+        for k, v in field_kwargs.items():
+            fstrs.append([
+                k_opts[k].format(self.render_field_name_sql(field_name)),
+                self.PLACEHOLDER,
+                v
+            ])
+
+        return fstrs
+
+    def render_datatype_int_sql(self, field_name, field, **kwargs):
+        if field.is_ref():
+            field_type = 'BIGINT' # INT8
+
+        else:
+            # https://www.postgresql.org/docs/current/datatype-numeric.html
+            size_info = field.size_info()
+            size = size_info["size"]
+
+            if size == 0:
+                field_type = 'INTEGER' # INT4
+
+            elif size <= 32767:
+                field_type = 'SMALLINT' # INT2
+
+            elif size <= 2147483647:
+                # INT4
+                field_type = 'SERIAL' if field.is_auto() else 'INTEGER'
+
+            elif size <= 9223372036854775807:
+                # INT8
+                field_type = 'BIGSERIAL' if field.is_auto() else 'BIGINT'
+
+            else:
+                precision = size_info["precision"]
+                field_type = f'NUMERIC({precision}, 0)'
+
+        return field_type
+
+    def render_datatype_str_sql(self, field_name, field, **kwargs):
+        if field.interface_options.get('ignore_case', False):
+            kwargs.setdefault("datatype", "CITEXT")
+
+        return super().render_datatype_str_sql(field_name, field, **kwargs)
+
+    def render_datatype_datetime_sql(self, field_name, field, **kwargs):
+        # http://www.postgresql.org/docs/9.0/interactive/datatype-datetime.html
+        #field_type = 'TIMESTAMP WITHOUT TIME ZONE'
+
+        # https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_timestamp_.28without_time_zone.29
+        return 'TIMESTAMPTZ'
+
+    def render_datatype_dict_sql(self, field_name, field, **kwargs):
+        # https://www.postgresql.org/docs/current/datatype-json.html
+        # In general, most applications should prefer to store JSON data as
+        # jsonb, unless there are quite specialized needs
+        return 'JSONB'
+
+    def render_datatype_float_sql(self, field_name, field, **kwargs):
+        """
+        https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
+        """
+        size_info = field.size_info()
+        if size_info["has_precision"]:
+            precision = size_info["precision"]
+            scale = size_info["scale"]
+            field_type = f'NUMERIC({precision}, {scale})'
+
+        else:
+            size = size_info["size"]
+
+            # https://learn.microsoft.com/en-us/cpp/c-language/type-float
+            if size < 3.402823466e+38:
+                #field_type = 'REAL'
+                # REAL only has 6 digits after the period, double precision has
+                # 15 digits after the period
+                field_type = 'DOUBLE PRECISION'
+
+            elif size < 1.7976931348623158e+308:
+                field_type = 'DOUBLE PRECISION'
+
+            else:
+                precision = size_info["precision"]
+                field_type = f'NUMERIC({precision})'
+
+        return field_type
+
+    def render_datatype_bytes_sql(self, field_name, field, **kwargs):
+        """Why Postgres? Why?
+
+        https://www.postgresql.org/docs/current/datatype-binary.html
+            The SQL standard defines a different binary string type, called BLOB
+            or BINARY LARGE OBJECT. The input format is different from bytea,
+            but the provided functions and operators are mostly the same.
+        """
+        return 'BYTEA'
+
+    def render_datatype_uuid_sql(self, field_name, field, **kwargs):
+        # https://www.postgresql.org/docs/current/datatype-uuid.html
+        # https://www.postgresql.org/docs/current/functions-uuid.html
+        field_type = 'UUID'
+        if field.is_auto():
+            field_type += ' DEFAULT gen_random_uuid()'
+
+        return field_type
+
+    def create_error(self, e, **kwargs):
+        """
+        https://www.psycopg.org/docs/module.html#exceptions
+        """
+        kwargs.setdefault("error_module", psycopg)
+        if isinstance(e, psycopg.ProgrammingError):
+            e_msg = String(e)
+            if "does not exist" in e_msg:
+                if "column" in e_msg:
+                    # INSERT: column "foo" of relation "<TABLE-NAME>" does not
+                    #   exist
+                    # SELECT: column "foo" does not exist
+                    e = FieldError(e)
+
+                else:
+                    #'relation "<TABLE-NAME>" does not exit'
+                    e = TableError(e)
+
+            elif "cannot adapt type" in e_msg:
+                ms = re.search(r"type\s'(\S+)'", e_msg)
+                type_name = ms.group(1)
+                if error_args := kwargs.get("error_args", []):
+                    for index, value in enumerate(error_args[1]):
+                        if type_name in str(type(value)):
+                            e = PlaceholderError(
+                                e,
+                                message=" ".join([
+                                    f"Placeholder {index} of query",
+                                    f"has unexpected type {type(value)}"
+                                ])
+                            )
+                            break
+
+                else:
+                    e = PlaceholderError(e)
+
+            elif (
+                "the query has" in e_msg
+                and "placeholders" in e_msg
+                and "parameters were passed" in e_msg
+            ):
+                e = PlaceholderError(e)
+
+            else:
+                e = super().create_error(e, **kwargs)
+
+        elif isinstance(e, psycopg.errors.AdminShutdown):
+            e = CloseError(e)
+
+        elif isinstance(e, psycopg.errors.InFailedSqlTransaction):
+            e = CloseError(e)
+
+        elif isinstance(e, psycopg.errors.IntegrityError):
+            e = UniqueError(e)
+
+        elif isinstance(e, IndexError):
+            e = PlaceholderError(e)
+
+        else:
+            e = super().create_error(e, **kwargs)
+
+        return e
+
diff --git a/prom/interface/postgres/__init__.py b/prom/interface/postgres/__init__.py
deleted file mode 100644
index af68acc..0000000
--- a/prom/interface/postgres/__init__.py
+++ /dev/null
@@ -1,574 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-http://pythonhosted.org/psycopg2/module.html
-
-http://zetcode.com/db/postgresqlpythontutorial/
-http://wiki.postgresql.org/wiki/Using_psycopg2_with_PostgreSQL
-http://pythonhosted.org/psycopg2/
-"""
-from __future__ import unicode_literals, division, print_function, absolute_import
-import re
-import os
-import sys
-import decimal
-import datetime
-import uuid
-import json
-from collections import Counter
-
-# third party
-import psycopg2
-import psycopg2.extras
-import psycopg2.extensions
-
-# first party
-from ..sql import SQLInterface, SQLConnection
-from ...compat import *
-from ...utils import get_objects
-from ...exception import (
-    InterfaceError,
-    UniqueError,
-    TableError,
-    FieldError,
-    UniqueError,
-    CloseError,
-    PlaceholderError,
-)
-
-
-# class LoggingCursor(psycopg2.extras.RealDictCursor):
-#     def execute(self, sql, args=None):
-#         logger.debug(self.mogrify(sql, args))
-#         super(LoggingCursor, self).execute(sql, args)
-#         #psycopg2.extensions.cursor.execute(self, sql, args)
-
-
-class DictType(psycopg2.extensions.QuotedString):
-    """Converts from python dict to JSONB to be saved into the db
-
-    https://www.psycopg.org/docs/extensions.html#psycopg2.extensions.QuotedString
-    https://www.psycopg.org/docs/extensions.html#psycopg2.extensions.register_adapter
-
-    Surprisingly, Postgres converts the json value in the db back to dict just fine
-    but does not adapt a dict, I'm not sure why
-    """
-    @classmethod
-    def adapt(cls, val):
-        """adapter should be a function taking a single argument (the object to adapt)
-        and returning an object conforming to the ISQLQuote protocol (e.g. exposing
-        a getquoted() method). Once an object is registered, it can be safely used
-        in SQL queries and by the adapt() function
-
-        :param val: dict, the value coming from Python destined for Postgres
-        :returns: str
-        """
-        return cls(json.dumps(val))
-
-
-class StringType(object):
-    @classmethod
-    def convert(cls, val, cur):
-        """Convert a db string to a python str type
-
-        https://www.psycopg.org/docs/extensions.html#psycopg2.extensions.new_type
-
-            These functions are used to manipulate type casters to convert from
-            PostgreSQL types to Python objects.
-
-            adapter should have signature fun(value, cur) where value is the string
-            representation returned by PostgreSQL and cur is the cursor from which
-            data are read. In case of NULL, value will be None. The adapter should
-            return the converted object.
-
-        :param val: str, the value coming from Postgres and destined for Python
-        :param cur: cursor
-        :returns: str
-        """
-        if isinstance(val, str) and val.startswith("\\x"):
-            buf = psycopg2.BINARY(val, cur)
-            val = bytes(buf).decode(cur.connection.encoding)
-
-        return val
-
-
-class PostgreSQLConnection(SQLConnection, psycopg2.extensions.connection):
-#class Connection(SQLConnection, psycopg2.extras.LoggingConnection):
-    """
-    https://www.psycopg.org/docs/connection.html
-    http://initd.org/psycopg/docs/advanced.html
-    http://initd.org/psycopg/docs/extensions.html#psycopg2.extensions.connection
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        # http://initd.org/psycopg/docs/connection.html#connection.autocommit
-        self.autocommit = True
-
-        psycopg2.extensions.register_type(
-            # https://www.psycopg.org/docs/extensions.html#psycopg2.extensions.new_type
-            psycopg2.extensions.new_type(psycopg2.STRING.values, "STRING", StringType.convert)
-        )
-
-        # https://www.psycopg.org/docs/extensions.html#psycopg2.extensions.register_adapter
-        psycopg2.extensions.register_adapter(dict, DictType.adapt)
-
-
-class PostgreSQL(SQLInterface):
-    """
-    https://www.psycopg.org/docs/
-    https://www.psycopg.org/docs/usage.html
-    """
-    LIMIT_NONE = "ALL"
-
-    _connection_pool = None
-
-    @classmethod
-    def configure(cls, config):
-        config = super().configure(config)
-        port = config.port
-        if not port:
-            config.port = 5432
-        return config
-
-    def get_paramstyle(self):
-        """
-        https://www.psycopg.org/docs/module.html#psycopg2.paramstyle
-        """
-        return psycopg2.paramstyle
-
-    def _connect(self, config):
-        # set pool_key to None to use the pool as an actual pool, right now we've
-        # turned it back into basically a sync interface since I don't need the gevent
-        # stuff right now and I want to switch to using the native async interface
-        self.config.options.setdefault("pool_key", "sync_interface")
-        minconn = int(config.options.get('pool_minconn', 1))
-        maxconn = int(config.options.get('pool_maxconn', 1))
-        pool_class_name = config.options.get(
-            'pool_class',
-            'psycopg2.pool.SimpleConnectionPool'
-        )
-
-        _, pool_class = get_objects(pool_class_name)
-
-        self.log_debug("connecting using pool class {}".format(pool_class_name))
-
-        # https://www.psycopg.org/docs/pool.html
-        # http://initd.org/psycopg/docs/module.html#psycopg2.connect
-        self._connection_pool = pool_class(
-            minconn,
-            maxconn,
-            dbname=config.database,
-            user=config.username,
-            password=config.password,
-            host=config.host,
-            port=config.port,
-            cursor_factory=psycopg2.extras.RealDictCursor,
-            #cursor_factory=LoggingCursor,
-            connection_factory=PostgreSQLConnection,
-        )
-
-        self._connection_pool.keys = Counter()
-
-    def _get_connection(self):
-        key = self.config.options["pool_key"]
-        connection = self._connection_pool.getconn(
-            key=key,
-        )
-
-        connection_id = id(connection)
-
-        # change the connection readonly status if they don't match
-        if bool(connection.readonly) != bool(self.config.readonly):
-            # https://www.psycopg.org/docs/connection.html#connection.readonly
-            self.log_warning([
-                f"Changing connection {connection_id:02x}",
-                f"to readonly={self.config.readonly}",
-                f"from readonly={connection.readonly}",
-            ])
-            connection.readonly = self.config.readonly
-
-        self._connection_pool.keys[key] += 1
-
-        return connection
-
-    def _free_connection(self, connection):
-        # if an error was handled there is a chance that the connection was reset
-        # and we don't want to put a dead connection back into the pool
-        key = self.config.options["pool_key"]
-        self._connection_pool.keys[key] -= 1
-
-        if connection.closed:
-            try:
-                self.log_warning(f"Discarding pool connection {id(connection):02x} because it is closed")
-                self._connection_pool.putconn(
-                    connection,
-                    key=key,
-                    close=True,
-                )
-
-            except KeyError:
-                # something happened and so we just discard this instead of
-                # trying to clean it up
-                pass
-
-        else:
-            if self._connection_pool.keys[key] <= 0:
-                self._connection_pool.putconn(
-                    connection,
-                    key=key,
-                )
-
-    def _close(self):
-        self._connection_pool.closeall()
-        self._connection_pool = None
-
-    def _readonly(self, readonly, **kwargs):
-        """readonly setting is handled when you grab the connection from get_connection()
-        so this method does nothing"""
-        pass
-
-    def _get_tables(self, table_name, **kwargs):
-        query_str = 'SELECT tablename FROM pg_tables WHERE tableowner = %s'
-        query_args = [self.config.username]
-
-        if table_name:
-            query_str += ' AND tablename = %s'
-            query_args.append(str(table_name))
-
-        schema_name = kwargs.get("schema_name", "public")
-        if schema_name:
-            query_str += ' AND schemaname = %s'
-            query_args.append(schema_name)
-
-        ret = self._raw(query_str, *query_args, **kwargs)
-        # http://www.postgresql.org/message-id/CA+mi_8Y6UXtAmYKKBZAHBoY7F6giuT5WfE0wi3hR44XXYDsXzg@mail.gmail.com
-        return [r['tablename'] for r in ret]
-
-    def _inserts(self, schema, field_names, field_values, **kwargs):
-        """
-        https://www.psycopg.org/docs/cursor.html#cursor.executemany
-        https://www.psycopg.org/docs/extras.html#fast-exec
-        https://www.psycopg.org/docs/extras.html#psycopg2.extras.execute_batch
-        """
-        query_str = self.render_inserts_sql(schema, field_names, **kwargs)
-        with self.connection(**kwargs) as connection:
-            cur = connection.cursor()
-            psycopg2.extras.execute_batch(
-                cur,
-                query_str,
-                field_values,
-            )
-
-    def _delete_table(self, schema, **kwargs):
-        """
-        https://www.postgresql.org/docs/current/sql-droptable.html
-        """
-        query_str = 'DROP TABLE IF EXISTS {} CASCADE'.format(self.render_table_name_sql(schema))
-        self._raw(query_str, ignore_result=True, **kwargs)
-
-    def _get_fields(self, table_name, **kwargs):
-        """return all the fields for the given schema"""
-        ret = {}
-        query_args = ['f', self.render_table_name_sql(table_name)]
-
-        # I had to brush up on my join knowledge while writing this query
-        # https://en.wikipedia.org/wiki/Join_(SQL)
-        #
-        # other helpful links
-        # * https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns
-        # * https://www.postgresql.org/docs/9.4/static/catalog-pg-attribute.html
-        # * https://www.postgresql.org/docs/9.3/static/catalog-pg-type.html
-        # 
-        # another approach
-        # * http://dba.stackexchange.com/questions/22362/
-        # * http://gis.stackexchange.com/questions/94049/
-        query_str = [
-            'SELECT',
-            ',  '.join([
-                'a.attnum',
-                'a.attname',
-                'a.attnotnull',
-                't.typname',
-                'i.indisprimary',
-                #'s.conname',
-                #'pg_get_constraintdef(s.oid, true) as condef',
-                'c.relname AS confrelname',
-            ]),
-            'FROM',
-            '  pg_attribute a',
-            'JOIN pg_type t ON a.atttypid = t.oid',
-            'LEFT JOIN pg_index i ON a.attrelid = i.indrelid',
-            '  AND a.attnum = any(i.indkey)',
-            'LEFT JOIN pg_constraint s ON a.attrelid = s.conrelid',
-            '  AND s.contype = {} AND a.attnum = any(s.conkey)'.format(self.PLACEHOLDER),
-            'LEFT JOIN pg_class c ON s.confrelid = c.oid',
-            'WHERE',
-            '  a.attrelid = {}::regclass'.format(self.PLACEHOLDER),
-            '  AND a.attisdropped = False',
-            '  AND a.attnum > 0',
-            'ORDER BY a.attnum ASC',
-        ]
-        query_str = "\n".join(query_str)
-        fields = self._raw(query_str, *query_args, **kwargs)
-
-        pg_types = {
-            "float4": float,
-            "float8": float,
-            "timestamp": datetime.datetime,
-            "timestamptz": datetime.datetime,
-            "int2": int,
-            "int4": int,
-            "int8": long,
-            "numeric": decimal.Decimal,
-            "text": str,
-            "citext": str,
-            "bpchar": str,
-            "varchar": str,
-            "bool": bool,
-            "date": datetime.date,
-            "blob": bytes,
-            "bytea": bytes,
-            "jsonb": bytes,
-            "uuid": uuid.UUID,
-        }
-
-        # the rows we can set: field_type, name, field_required, min_size, max_size,
-        #   size, unique, pk, <foreign key info>
-        # These keys will roughly correspond with schema.Field
-        for row in fields:
-            field = {
-                "name": row["attname"],
-                "field_type": pg_types[row["typname"]],
-                "field_required": row["attnotnull"],
-                "pk": bool(row["indisprimary"]),
-            }
-
-            if row["confrelname"]:
-                # TODO -- I can't decide which name I like
-                field["schema_table_name"] = row["confrelname"]
-                field["ref_table_name"] = row["confrelname"]
-
-            if row["typname"] == "citext":
-                field["ignore_case"] = True
-
-            ret[field["name"]] = field
-
-        return ret
-
-    def _get_indexes(self, schema, **kwargs):
-        """return all the indexes for the given schema"""
-        ret = {}
-        query_str = [
-            'SELECT',
-            '  tbl.relname AS table_name, i.relname AS index_name, a.attname AS field_name,',
-            '  ix.indkey AS index_order, a.attnum AS field_num',
-            'FROM',
-            '  pg_class tbl, pg_class i, pg_index ix, pg_attribute a',
-            'WHERE',
-            '  tbl.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = tbl.oid',
-            '  AND a.attnum = ANY(ix.indkey) AND tbl.relkind = %s AND tbl.relname = %s',
-            'ORDER BY',
-            '  tbl.relname, i.relname',
-        ]
-        query_str = "\n".join(query_str)
-
-        indexes = self._raw(query_str, 'r', str(schema), **kwargs)
-
-        # massage the data into more readable {index_name: fields} format
-        for idict in indexes:
-            if idict['index_name'] not in ret:
-                ret[idict['index_name']] = list(map(int, idict['index_order'].split(' ')))
-
-            i = ret[idict['index_name']].index(idict['field_num'])
-            ret[idict['index_name']][i] = idict['field_name']
-
-        return ret
-
-    def render_sort_field_sql(self, field_name, field_vals, sort_dir_str):
-        # this solution is based off:
-        # http://postgresql.1045698.n5.nabble.com/ORDER-BY-FIELD-feature-td1901324.html
-        # see also: https://gist.github.com/cpjolicoeur/3590737
-        query_sort_str = []
-        query_args = []
-        for v in reversed(field_vals):
-            query_sort_str.append('  {} = {} {}'.format(
-                self.render_field_name_sql(field_name), self.PLACEHOLDER, sort_dir_str))
-            query_args.append(v)
-
-        return ',\n'.join(query_sort_str), query_args
-
-    def render_date_field_sql(self, field_name, field_kwargs, symbol):
-        """
-        allow extracting information from date
-
-        http://www.postgresql.org/docs/8.3/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT
-        """
-        fstrs = []
-        k_opts = {
-            'century': 'EXTRACT(CENTURY FROM {})',
-            'day': 'EXTRACT(DAY FROM {})',
-            'decade': 'EXTRACT(DECADE FROM {})',
-            'weekday': 'EXTRACT(DOW FROM {})',
-            'dow': 'EXTRACT(DOW FROM {})',
-            'isodow': 'EXTRACT(ISODOW FROM {})',
-            'epoch': 'EXTRACT(EPOCH FROM {})',
-            'hour': 'EXTRACT(HOUR FROM {})',
-            'year': 'EXTRACT(YEAR FROM {})',
-            'isoyear': 'EXTRACT(ISOYEAR FROM {})',
-            'minute': 'EXTRACT(MINUTE FROM {})',
-            'month': 'EXTRACT(MONTH FROM {})',
-            'quarter': 'EXTRACT(QUARTER FROM {})',
-            'week': 'EXTRACT(WEEK FROM {})',
-        }
-
-        for k, v in field_kwargs.items():
-            fstrs.append([k_opts[k].format(self.render_field_name_sql(field_name)), self.PLACEHOLDER, v])
-
-        return fstrs
-
-    def render_datatype_int_sql(self, field_name, field, **kwargs):
-        if field.is_ref():
-            field_type = 'BIGINT' # INT8
-
-        else:
-            # https://www.postgresql.org/docs/current/datatype-numeric.html
-            size_info = field.size_info()
-            size = size_info["size"]
-
-            if size == 0:
-                field_type = 'INTEGER' # INT4
-
-            elif size <= 32767:
-                field_type = 'SMALLINT' # INT2
-
-            elif size <= 2147483647:
-                # INT4
-                field_type = 'SERIAL' if field.is_auto() else 'INTEGER'
-
-            elif size <= 9223372036854775807:
-                # INT8
-                field_type = 'BIGSERIAL' if field.is_auto() else 'BIGINT'
-
-            else:
-                precision = size_info["precision"]
-                field_type = f'NUMERIC({precision}, 0)'
-
-        return field_type
-
-    def render_datatype_str_sql(self, field_name, field, **kwargs):
-        if field.interface_options.get('ignore_case', False):
-            kwargs.setdefault("datatype", "CITEXT")
-
-        return super().render_datatype_str_sql(field_name, field, **kwargs)
-
-    def render_datatype_datetime_sql(self, field_name, field, **kwargs):
-        # http://www.postgresql.org/docs/9.0/interactive/datatype-datetime.html
-        #field_type = 'TIMESTAMP WITHOUT TIME ZONE'
-
-        # https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_timestamp_.28without_time_zone.29
-        return 'TIMESTAMPTZ'
-
-    def render_datatype_dict_sql(self, field_name, field, **kwargs):
-        # https://www.postgresql.org/docs/current/datatype-json.html
-        # In general, most applications should prefer to store JSON data as
-        # jsonb, unless there are quite specialized needs
-        return 'JSONB'
-
-    def render_datatype_float_sql(self, field_name, field, **kwargs):
-        """
-        https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
-        """
-        size_info = field.size_info()
-        if size_info["has_precision"]:
-            precision = size_info["precision"]
-            scale = size_info["scale"]
-            field_type = f'NUMERIC({precision}, {scale})'
-
-        else:
-            size = size_info["size"]
-
-            # https://learn.microsoft.com/en-us/cpp/c-language/type-float
-            if size < 3.402823466e+38:
-                #field_type = 'REAL'
-                # REAL only has 6 digits after the period, double precision has
-                # 15 digits after the period
-                field_type = 'DOUBLE PRECISION'
-
-            elif size < 1.7976931348623158e+308:
-                field_type = 'DOUBLE PRECISION'
-
-            else:
-                precision = size_info["precision"]
-                field_type = f'NUMERIC({precision})'
-
-        return field_type
-
-    def render_datatype_bytes_sql(self, field_name, field, **kwargs):
-        """Why Postgres? Why?
-
-        https://www.postgresql.org/docs/current/datatype-binary.html
-            The SQL standard defines a different binary string type, called BLOB
-            or BINARY LARGE OBJECT. The input format is different from bytea, but
-            the provided functions and operators are mostly the same.
-        """
-        return 'BYTEA'
-
-    def render_datatype_uuid_sql(self, field_name, field, **kwargs):
-        # https://www.postgresql.org/docs/current/datatype-uuid.html
-        # https://www.postgresql.org/docs/current/functions-uuid.html
-        field_type = 'UUID'
-        if field.is_auto():
-            field_type += ' DEFAULT gen_random_uuid()'
-
-        return field_type
-
-    def create_error(self, e, **kwargs):
-        """
-        https://www.psycopg.org/docs/module.html#exceptions
-        """
-        kwargs.setdefault("error_module", psycopg2)
-        if isinstance(e, psycopg2.ProgrammingError):
-            e_msg = String(e)
-            if "does not exist" in e_msg:
-                if "column" in e_msg:
-                    #INSERT: column "foo" of relation "<TABLE-NAME>" does not exist
-                    #SELECT: column "foo" does not exist
-                    e = FieldError(e)
-
-                else:
-                    #'relation "<TABLE-NAME>" does not exit'
-                    e = TableError(e)
-
-            elif "can't adapt type" in e_msg and "error_args" in kwargs:
-                error_args = kwargs["error_args"]
-                ms = re.search(r"type\s'(\S+)'", e_msg)
-                type_name = ms.group(1)
-                for index, value in enumerate(error_args[1]):
-                    if type_name in str(type(value)):
-                        e = PlaceholderError(
-                            e,
-                            *error_args,
-                            message=f"Placeholder {index} of query has unexpected type {type(value)}",
-                        )
-                        break
-
-            else:
-                e = super().create_error(e, **kwargs)
-
-        elif isinstance(e, psycopg2.errors.AdminShutdown):
-            e = CloseError(e)
-
-        elif isinstance(e, psycopg2.errors.InFailedSqlTransaction):
-            e = CloseError(e)
-
-        elif isinstance(e, psycopg2.IntegrityError):
-            e = UniqueError(e)
-
-        elif isinstance(e, IndexError) and "error_args" in kwargs:
-            e = PlaceholderError(e, *kwargs.get("error_args", []))
-
-        else:
-            e = super().create_error(e, **kwargs)
-
-        return e
-
diff --git a/prom/interface/postgres/gevent.py b/prom/interface/postgres/gevent.py
deleted file mode 100644
index 2eeecdd..0000000
--- a/prom/interface/postgres/gevent.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import logging
-
-import psycogreen.gevent
-import gevent.monkey
-from gevent.queue import Queue
-import psycopg2
-from psycopg2 import extensions, OperationalError, connect
-from psycopg2.pool import AbstractConnectionPool, PoolError
-
-from .. import get_interfaces
-from . import PostgreSQL
-
-
-logger = logging.getLogger(__name__)
-
-
-def patch_all(maxconn=10, **kwargs):
-    if not gevent.monkey.saved:
-        # https://github.com/gevent/gevent/blob/master/src/gevent/monkey.py
-        logger.warning("Running gevent.monkey.patch_all() since not run previously")
-        gevent.monkey.patch_all()
-
-    psycogreen.gevent.patch_psycopg()
-
-    kwargs.setdefault('pool_maxconn', maxconn)
-    kwargs.setdefault('pool_class', '{}.ConnectionPool'.format(__name__))
-    kwargs.setdefault('async', True)
-    for name, interface in get_interfaces().items():
-        if isinstance(interface, PostgreSQL):
-            interface.close()
-            interface.config.options.update(kwargs)
-
-
-class ConnectionPool(AbstractConnectionPool):
-    """a gevent green thread safe connection pool
-    this is based off an example found here
-    https://github.com/surfly/gevent/blob/master/examples/psycopg2_pool.py
-
-    https://github.com/psycopg/psycopg2/blob/master/lib/pool.py
-    """
-    def __init__(self, minconn, maxconn, *args, **kwargs):
-        self.size = 0
-        self.minconn = minconn
-        self.maxconn = maxconn
-        self.closed = False
-
-        self._args = args
-        self._kwargs = kwargs
-        self._pool = Queue()
-        self._used = {} # required for interface compatibility
-
-        for i in range(self.minconn):
-            connection = self._connect()
-            self.putconn(connection)
-
-    def _connect(self, key=None):
-        self.size += 1
-        try:
-            # I think connect() causes a WAIT, so we need to increment size before
-            # we call connect() in order to actually make it increment
-            conn = psycopg2.connect(*self._args, **self._kwargs)
-
-        except:
-            self.size -= 1
-            raise
-
-        return conn
-
-    def getconn(self, key=None):
-        if self.closed: raise PoolError("connection pool is closed")
-        pool = self._pool
-        if self.size >= self.maxconn or pool.qsize():
-            conn = pool.get()
-
-        else:
-            try:
-                conn = self._connect()
-
-            except:
-                raise
-
-        return conn
-
-    def putconn(self, conn=None, key=None, close=False):
-        if self.closed: raise PoolError("connection pool is closed")
-        if close:
-            conn.close()
-        else:
-            self._pool.put(conn)
-
-    def closeall(self):
-        if self.closed: raise PoolError("connection pool is closed")
-        # TODO -- might be better to do this decrementing self.size?
-        while not self._pool.empty():
-            conn = self._pool.get_nowait()
-            try:
-                conn.close()
-            except Exception:
-                pass
-
-        self.closed = True
diff --git a/prom/interface/sql.py b/prom/interface/sql.py
index 5235769..3b94efb 100644
--- a/prom/interface/sql.py
+++ b/prom/interface/sql.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import os
 import datetime
 import decimal
 import logging
 import uuid
 from functools import cached_property
+import inspect
 
 from ..query import Query
 from ..exception import (
@@ -29,46 +29,40 @@ class SQLConnection(Connection):
     https://www.sqlite.org/lockingv3.html
     https://www.sqlite.org/lang_transaction.html
     """
-    def _transaction_start(self):
-        self._execute("BEGIN")
+    async def _transaction_start(self):
+        await self.execute("BEGIN")
 
-    def _transaction_starting(self, tx):
+    async def _transaction_starting(self, tx):
         # http://www.postgresql.org/docs/9.2/static/sql-savepoint.html
-        self._execute("SAVEPOINT {}".format(
+        await self.execute("SAVEPOINT {}".format(
             self.interface.render_field_name_sql(tx["name"])
         ))
 
-    def _transaction_stop(self):
+    async def _transaction_stop(self):
         """
         http://initd.org/psycopg/docs/usage.html#transactions-control
         https://news.ycombinator.com/item?id=4269241
         """
-        self._execute("COMMIT")
+        await self.execute("COMMIT")
 
-    def _transaction_stopping(self, tx):
-        self._execute("RELEASE {}".format(
+    async def _transaction_stopping(self, tx):
+        await self.execute("RELEASE {}".format(
             self.interface.render_field_name_sql(tx["name"])
         ))
 
-    def _transaction_fail(self):
-        self._execute("ROLLBACK")
+    async def _transaction_fail(self):
+        await self.execute("ROLLBACK")
 
-    def _transaction_failing(self, tx):
+    async def _transaction_failing(self, tx):
         # http://www.postgresql.org/docs/9.2/static/sql-rollback-to.html
-        self._execute("ROLLBACK TO SAVEPOINT {}".format(
+        await self.execute("ROLLBACK TO SAVEPOINT {}".format(
             self.interface.render_field_name_sql(tx["name"])
         ))
 
-    def _execute(self, query_str):
-        self.log_info(f"{self} - {query_str}")
-        cur = self.cursor()
-        cur.execute(query_str)
-        cur.close()
-
 
 class SQLInterfaceABC(Interface):
-    """SQL database interfaces should extend SQLInterface and implement all these
-    methods in this class and all the methods in InterfaceABC"""
+    """SQL database interfaces should extend SQLInterface and implement all
+    these methods in this class and all the methods in InterfaceABC"""
     @property
     def LIMIT_NONE(self):
         """When an offset is set but not a limit, this is the value that will be
@@ -76,12 +70,14 @@ def LIMIT_NONE(self):
 
         :returns: str|int|None
         """
-        raise NotImplementedError("this property should be set in any children class")
+        raise NotImplementedError(
+            "this property should be set in a child class"
+        )
 
     def get_paramstyle(self):
-        """Returns the paramstyle that is used by self.PLACEHOLDER to decide what
-        val placeholder to use when building queries. This would also be the value
-        to use when calling .raw().
+        """Returns the paramstyle that is used by self.PLACEHOLDER to decide
+        what val placeholder to use when building queries. This would also be
+        the value to use when calling .raw().
 
         The dbapi 2.0 spec requires this to be set
 
@@ -92,25 +88,25 @@ def get_paramstyle(self):
         """
         raise NotImplemented()
 
-    def render_date_field_sql(self, field_name, field_kwargs, symbol):
+    async def render_date_field_sql(self, field_name, field_kwargs, symbol):
         raise NotImplemented()
 
-    def render_sort_field_sql(self, field_name, field_vals, sort_dir_str):
+    async def render_sort_field_sql(self, field_name, field_vals, sort_dir_str):
         """normalize the sort string
 
         return -- tuple -- field_sort_str, field_sort_args"""
         raise NotImplemented()
 
-    def render_nolimit_sql(limit, **kwargs):
+    async def render_nolimit_sql(limit, **kwargs):
         raise NotImplemented()
 
-    def render_datatype_datetime_sql(self, field_name, field, **kwargs):
+    async def render_datatype_datetime_sql(self, field_name, field, **kwargs):
         raise NotImplementedError()
 
-    def render_datatype_dict_sql(self, field_name, field, **kwargs):
+    async def render_datatype_dict_sql(self, field_name, field, **kwargs):
         raise NotImplementedError()
 
-    def render_datatype_uuid_sql(self, field_name, field, **kwargs):
+    async def render_datatype_uuid_sql(self, field_name, field, **kwargs):
         raise NotImplementedError()
 
 
@@ -124,9 +120,9 @@ def PLACEHOLDER(self):
 
         https://www.psycopg.org/docs/usage.html#passing-parameters-to-sql-queries
 
-        NOTE -- It looks like both SQLite and Postgres support "named" so if you
-            want to use .raw() queries that will work in both interface I would use
-            named parameters
+        NOTE -- It looks like both SQLite and Postgres support "named" so if
+        you want to use .raw() queries that will work in both interface I would
+        use named parameters
 
         :returns: str, usually something like "?" or "%s"
         """
@@ -150,33 +146,38 @@ def PLACEHOLDER(self):
         else:
             raise NotImplementedError(f"Unknown paramstyle {paramstyle}")
 
-    def _set_table(self, schema, **kwargs):
+    async def _set_table(self, schema, **kwargs):
         """
         http://sqlite.org/lang_createtable.html
         http://www.postgresql.org/docs/9.1/static/sql-createtable.html
         http://www.postgresql.org/docs/8.1/static/datatype.html
         http://pythonhosted.org/psycopg2/usage.html#adaptation-of-python-values-to-sql-types
         """
-        query_str = []
-        query_str.append("CREATE TABLE IF NOT EXISTS {} (".format(self.render_table_name_sql(schema)))
+        query_str = [
+            "CREATE TABLE IF NOT EXISTS {} (".format(
+                self.render_table_name_sql(schema)
+            )
+        ]
 
         query_fields = []
         for field_name, field in schema.fields.items():
-            query_fields.append('  {}'.format(self.render_datatype_sql(field_name, field)))
-
+            query_fields.append("  {}".format(
+                self.render_datatype_sql(field_name, field)
+            ))
         query_str.append(",\n".join(query_fields))
-        query_str.append(')')
+
+        query_str.append(")")
         query_str = "\n".join(query_str)
-        self._raw(query_str, ignore_result=True, **kwargs)
+        await self._raw(query_str, ignore_result=True, **kwargs)
 
     def _set_index(self, schema, name, field_names, **kwargs):
         """
-        NOTE -- we set the index name using <table_name>_<name> format since indexes have to have
-        a globally unique name in postgres
+        NOTE -- we set the index name using <table_name>_<name> format since
+        indexes have to have a globally unique name in postgres
 
         * https://www.sqlite.org/lang_createindex.html
-        * https://www.postgresql.org/docs/14/sql-createindex.html - "IF NOT EXISTS support
-        was added around 9.5
+        * https://www.postgresql.org/docs/14/sql-createindex.html - "IF NOT
+            EXISTS support was added around 9.5
         """
         query_str = 'CREATE {}INDEX IF NOT EXISTS {} ON {} ({})'.format(
             'UNIQUE ' if kwargs.get('unique', False) else '',
@@ -187,7 +188,7 @@ def _set_index(self, schema, name, field_names, **kwargs):
 
         return self._raw(query_str, ignore_result=True, **kwargs)
 
-    def _insert(self, schema, fields, **kwargs):
+    async def _insert(self, schema, fields, **kwargs):
         pk_names = schema.pk_names
         kwargs.setdefault("ignore_return_clause", len(pk_names) == 0)
         kwargs.setdefault("ignore_result", len(pk_names) == 0)
@@ -198,7 +199,7 @@ def _insert(self, schema, fields, **kwargs):
             **kwargs,
         )
 
-        r = self._raw(query_str, *query_args, **kwargs)
+        r = await self._raw(query_str, *query_args, **kwargs)
         if r and pk_names:
             if len(pk_names) > 1:
                 r = r[0]
@@ -206,7 +207,32 @@ def _insert(self, schema, fields, **kwargs):
                 r = r[0][pk_names[0]]
         return r
 
-    def _update(self, schema, fields, query, **kwargs):
+    async def _inserts(self, schema, field_names, field_values, **kwargs):
+        """Do a mutli insert
+
+        The query this will run is roughly:
+
+            INSERT INTO
+                (<field_names>)
+            VALUES
+                (tuple for tuple in <field_values>)
+
+        :param schema: Schema, the table
+        :param field_names: list[str], the field names
+        :param field_values: Iterable[Iterable[Any]], each row in the iterable
+            is a tuple of values that correspond to field_names
+        :param **kwargs: passed through
+        """
+        query_str = self.render_inserts_sql(schema, field_names, **kwargs)
+        await self._raw(
+            query_str,
+            *field_values,
+            ignore_result=True,
+            execute_many=True,
+            **kwargs
+        )
+
+    async def _update(self, schema, fields, query, **kwargs):
         query_str, query_args = self.render_update_sql(
             schema,
             fields,
@@ -214,9 +240,21 @@ def _update(self, schema, fields, query, **kwargs):
             **kwargs,
         )
 
-        return self._raw(query_str, *query_args, count_result=True, **kwargs)
+        return await self._raw(
+            query_str,
+            *query_args,
+            count_result=True,
+            **kwargs
+        )
 
-    def _upsert(self, schema, insert_fields, update_fields, conflict_field_names, **kwargs):
+    async def _upsert(
+        self,
+        schema,
+        insert_fields,
+        update_fields,
+        conflict_field_names,
+        **kwargs
+    ):
         """
         https://www.sqlite.org/lang_UPSERT.html
         """
@@ -226,8 +264,9 @@ def _upsert(self, schema, insert_fields, update_fields, conflict_field_names, **
         for field_name in conflict_field_names:
             # conflict fields need to be in the insert fields
             if field_name not in insert_fields:
+                errmsg = "Upsert insert fields on {} missing conflict field {}"
                 raise ValueError(
-                    "Upsert insert fields on {} missing conflict field {}".format(
+                    errmsg.format(
                         schema,
                         field_name,
                     )
@@ -236,8 +275,9 @@ def _upsert(self, schema, insert_fields, update_fields, conflict_field_names, **
             # conflict fields should not be in the update fields (this is more
             # for safety, they should use .update if they want to change them)
             if field_name in update_fields:
+                errmsg = "Upsert update fields on {} contains conflict field {}"
                 raise ValueError(
-                    "Upsert update fields on {} contains conflict field {}".format(
+                    errmsg.format(
                         schema,
                         field_name,
                     )
@@ -272,7 +312,7 @@ def _upsert(self, schema, insert_fields, update_fields, conflict_field_names, **
             )
             query_args = insert_args + update_args
 
-        r = self._raw(query_str, *query_args, **kwargs)
+        r = await self._raw(query_str, *query_args, **kwargs)
         if r and returning_field_names:
             if len(returning_field_names) > 1:
                 r = r[0]
@@ -280,33 +320,54 @@ def _upsert(self, schema, insert_fields, update_fields, conflict_field_names, **
                 r = r[0][returning_field_names[0]]
         return r
 
-    def _delete(self, schema, query, **kwargs):
-        where_query_str, query_args = self.render_sql(schema, query, only_where_clause=True)
+    async def _delete(self, schema, query, **kwargs):
+        where_query_str, query_args = self.render_sql(
+            schema,
+            query,
+            only_where_clause=True
+        )
         query_str = []
         query_str.append('DELETE FROM')
         query_str.append('  {}'.format(self.render_table_name_sql(schema)))
         query_str.append(where_query_str)
         query_str = "\n".join(query_str)
-        ret = self._raw(query_str, *query_args, count_result=True, **kwargs)
-        return ret
+        return await self._raw(
+            query_str,
+            *query_args,
+            count_result=True,
+            **kwargs
+        )
 
-    def _raw(self, query_str, *query_args, **kwargs):
+    async def _raw(self, query_str, *query_args, **kwargs):
         """
-        **kwargs -- dict
-            ignore_result -- boolean -- true to not attempt to fetch results
-            fetchone -- boolean -- true to only fetch one result
-            count_result -- boolean -- true to return the int count of rows affected
+        :param **kwargs:
+            - ignore_result: bool, true to not attempt to fetch results
+            - fetchone: bool, true to only fetch one result
+            - count_result: bool, true to return the int count of rows affected
+            - execute_many: bool, True to call cursor.executemany instead of
+                cursor.execute
         """
         ret = True
         # http://stackoverflow.com/questions/6739355/dictcursor-doesnt-seem-to-work-under-psycopg2
         #connection = kwargs.get('connection', None)
-        with self.connection(**kwargs) as connection:
+        async with self.connection(**kwargs) as connection:
             cur = connection.cursor()
-
-            ignore_result = kwargs.get('ignore_result', False)
-            count_result = kwargs.get('count_result', False)
-            one_result = kwargs.get('fetchone', kwargs.get('one_result', False))
-            cursor_result = kwargs.get('cursor_result', False)
+            # depending on the api, cursor() could either return a coroutine
+            # or something different like a cursor class instance
+            if inspect.isawaitable(cur):
+                cur = await cur
+
+            ignore_result = kwargs.get("ignore_result", False)
+            count_result = kwargs.get("count_result", False)
+            one_result = kwargs.get(
+                "fetchone",
+                kwargs.get("one_result", False)
+            )
+            cursor_result = kwargs.get("cursor_result", False)
+            execute_many = kwargs.get(
+                "execute_many",
+                kwargs.get("executemany", False)
+            )
 
             if cursor_result:
                 # https://stackoverflow.com/a/125140
@@ -314,37 +375,49 @@ def _raw(self, query_str, *query_args, **kwargs):
 
             if query_args:
                 self.log_for(
-                    debug=(["0x{} - {}\n{}", connection, query_str, query_args],),
-                    info=([f"{connection} - {query_str}"],)
+                    debug=(
+                        ["{} - {}\n{}", connection, query_str, query_args],
+                    ),
+                    info=(["{} - {}", connection, query_str],)
                 )
 
-                try:
-                    cur.execute(query_str, query_args)
-
-                except Exception as e:
-                    self.raise_error(
-                        e,
-                        error_args=[
-                            query_str,
-                            query_args,
-                            self.PLACEHOLDER,
-                        ]
-                    )
+                execute_args = [query_str, query_args]
 
             else:
-                self.log_info(f"{connection} - {query_str}")
-                cur.execute(query_str)
+                self.log_info("{} - {}", connection, query_str)
+
+                execute_args = [query_str]
+
+            try:
+                if execute_many:
+                    # https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executemany
+                    # https://www.psycopg.org/psycopg3/docs/api/cursors.html#psycopg.AsyncCursor.executemany
+                    # https://www.psycopg.org/docs/cursor.html#cursor.executemany
+                    await cur.executemany(*execute_args)
+
+                else:
+                    await cur.execute(*execute_args)
+
+            except Exception as e:
+                await self.raise_error(
+                    e,
+                    error_args=[
+                        query_str,
+                        query_args,
+                        self.PLACEHOLDER,
+                    ]
+                )
 
             if cursor_result:
                 ret = cur
 
             elif ignore_result:
-                cur.close()
+                await cur.close()
 
             else:
                 if one_result:
                     # https://www.psycopg.org/docs/cursor.html#cursor.fetchone
-                    ret = cur.fetchone()
+                    ret = await cur.fetchone()
 
                 elif count_result:
                     # https://www.psycopg.org/docs/cursor.html#cursor.rowcount
@@ -352,22 +425,22 @@ def _raw(self, query_str, *query_args, **kwargs):
 
                 else:
                     # https://www.psycopg.org/docs/cursor.html#cursor.fetchall
-                    ret = cur.fetchall()
+                    ret = await cur.fetchall()
 
-                cur.close()
+                await cur.close()
 
             return ret
 
-    def _get(self, schema, query, **kwargs):
+    async def _get(self, schema, query, **kwargs):
         """
         https://www.sqlite.org/lang_select.html
         """
         query_str, query_args = self.render_sql(schema, query)
-        return self._raw(query_str, *query_args, **kwargs)
+        return await self._raw(query_str, *query_args, **kwargs)
 
-    def _count(self, schema, query, **kwargs):
+    async def _count(self, schema, query, **kwargs):
         query_str, query_args = self.render_sql(schema, query, count_query=True)
-        ret = self._raw(query_str, *query_args, **kwargs)
+        ret = await self._raw(query_str, *query_args, **kwargs)
         if ret:
             ret = int(ret[0]['ct'])
         else:
@@ -375,54 +448,63 @@ def _count(self, schema, query, **kwargs):
 
         return ret
 
-    def _handle_field_error(self, schema, e, **kwargs):
-        """
-        this will add fields that don't exist in the table if they can be set to NULL,
-        the reason they have to be NULL is adding fields to Postgres that can be NULL
-        is really light, but if they have a default value, then it can be costly
+    async def _handle_field_error(self, schema, e, **kwargs):
+        """This will add fields that don't exist in the table if they can be
+        set to NULL, the reason they have to be NULL is adding fields to
+        Postgres that can be NULL is really light, but if they have a default
+        value, then it can be costly
         """
-        current_fields = self._get_fields(schema, **kwargs)
+        current_fields = await self._get_fields(schema, **kwargs)
         for field_name, field in schema.fields.items():
             if field_name not in current_fields:
                 if field.required:
-                    self.log_error(f"Cannot safely add {field_name} on the fly because it is required")
+                    self.log_error(
+                        "Required field {} cannot be safely add on the fly",
+                        field_name
+                    )
                     return False
 
                 else:
                     query_str = []
                     query_str.append('ALTER TABLE')
-                    query_str.append('  {}'.format(self.render_table_name_sql(schema)))
+                    query_str.append('  {}'.format(
+                        self.render_table_name_sql(schema)
+                    ))
                     query_str.append('ADD COLUMN')
-                    query_str.append('  {}'.format(self.render_datatype_sql(field_name, field)))
+                    query_str.append('  {}'.format(
+                        self.render_datatype_sql(field_name, field)
+                    ))
                     query_str = "\n".join(query_str)
-                    self._raw(query_str, ignore_result=True, **kwargs)
+                    await self._raw(query_str, ignore_result=True, **kwargs)
 
         return True
 
-    def _handle_table_error(self, schema, e, **kwargs):
+    async def _handle_table_error(self, schema, e, **kwargs):
         """
-        You can run into a problem when you are trying to set a table and it has a 
-        foreign key to a table that doesn't exist, so this method will go through 
-        all fk refs and make sure all the tables exist
+        You can run into a problem when you are trying to set a table and it
+        has a foreign key to a table that doesn't exist, so this method will go
+        through all fk refs and make sure all the tables exist
         """
         if query := kwargs.pop("query", None):
             if schemas := query.schemas:
                 for s in schemas:
-                    self.log_warning(f"Verifying {schema} query foreign key table: {s}")
-                    if not self._handle_table_error(s, e=e, **kwargs):
+                    self.log_warning(
+                        f"Verifying {schema} query foreign key table: {s}"
+                    )
+                    if not await self._handle_table_error(s, e=e, **kwargs):
                         return False
 
         for field_name, field_val in schema.fields.items():
             s = field_val.schema
             if s:
                 self.log_warning(f"Verifying {schema} foreign key table: {s}")
-                if not self._handle_table_error(schema=s, e=e, **kwargs):
+                if not await self._handle_table_error(schema=s, e=e, **kwargs):
                     return False
 
         # now that we know all fk tables exist, create this table
         # !!! This uses the external .set_table so it will run through all the 
         # indexes also
-        self.set_table(schema, **kwargs)
+        await self.set_table(schema, **kwargs)
         return True
 
     def render_table_name_sql(self, schema):
@@ -480,7 +562,9 @@ def render_select_sql(self, schema, query, **kwargs):
             query_str.append('FROM')
 
             if not query.compounds:
-                query_str.append("  {}".format(self.render_table_name_sql(schema)))
+                query_str.append("  {}".format(
+                    self.render_table_name_sql(schema)
+                ))
 
         return query_str, query_args
 
@@ -526,8 +610,11 @@ def render_field_sql(self, schema, field):
         symbol_map = {
             'in': {'symbol': 'IN', 'list': True},
             'nin': {'symbol': 'NOT IN', 'list': True},
-            'eq': {'symbol': '=', 'none_symbol': 'IS'},
-            'ne': {'symbol': '!=', 'none_symbol': 'IS NOT'},
+            # Previously we used "IS" and "IS NOT" but psycopg3 changed behavior
+            # https://stackoverflow.com/a/76396765
+            # https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#you-cannot-use-is-s
+            'eq': {'symbol': '=', 'none_symbol': 'IS NOT DISTINCT FROM'},
+            'ne': {'symbol': '!=', 'none_symbol': 'IS DISTINCT FROM'},
             'gt': {'symbol': '>'},
             'gte': {'symbol': '>='},
             'lt': {'symbol': '<'},
@@ -601,9 +688,10 @@ def render_field_sql(self, schema, field):
                     # the idea here is this is a condition that will
                     # automatically cause the query to fail but not necessarily 
                     # be an error, the best example is the IN (...) queries, if
-                    # you do self.in_foo([]).get() that will fail because the list
-                    # was empty, but a value error shouldn't be raised because a
-                    # common case is: self.if_foo(Bar.query.is_che(True).pks).get()
+                    # you do self.in_foo([]).get() that will fail because the
+                    # list was empty, but a value error shouldn't be raised
+                    # because a common case is:
+                    #   self.if_foo(Bar.query.is_che(True).pks).get()
                     # which should result in an empty set if there are no rows
                     # where che = TRUE
                     #
@@ -714,7 +802,10 @@ def render_sort_sql(self, schema, query, **kwargs):
                     query_args.extend(field_sort_args)
 
                 else:
-                    query_sort_str.append('  {} {}'.format(field.name, sort_dir_str))
+                    query_sort_str.append('  {} {}'.format(
+                        field.name,
+                        sort_dir_str
+                    ))
 
             query_str.append(',\n'.join(query_sort_str))
 
@@ -823,7 +914,9 @@ def render_insert_sql(self, schema, fields, **kwargs):
             # https://www.sqlite.org/lang_returning.html
             pk_name = schema.pk_name
             if pk_name:
-                query_str += ' RETURNING {}'.format(self.render_field_name_sql(pk_name))
+                query_str += ' RETURNING {}'.format(
+                    self.render_field_name_sql(pk_name)
+                )
 
         return query_str, query_vals
 
@@ -852,13 +945,20 @@ def render_update_sql(self, schema, fields, query, **kwargs):
 
         field_str = []
         for field_name, field_val in fields.items():
-            field_str.append('{} = {}'.format(self.render_field_name_sql(field_name), self.PLACEHOLDER))
+            field_str.append('{} = {}'.format(
+                self.render_field_name_sql(field_name),
+                self.PLACEHOLDER
+            ))
             query_args.append(field_val)
 
         query_str += 'SET {}'.format(',\n'.join(field_str))
 
         if query:
-            where_query_str, where_query_args = self.render_sql(schema, query, only_where_clause=True)
+            where_query_str, where_query_args = self.render_sql(
+                schema,
+                query,
+                only_where_clause=True
+            )
             query_str += ' {}'.format(where_query_str)
             query_args.extend(where_query_args)
 
@@ -917,9 +1017,15 @@ def render_datatype_sql(self, field_name, field):
 
         else:
             if field.is_ref():
-                field_type += ' ' + self.render_datatype_ref_sql(field_name, field)
+                field_type += ' ' + self.render_datatype_ref_sql(
+                    field_name,
+                    field
+                )
 
-        return '{} {}'.format(self.render_field_name_sql(field_name), field_type)
+        return '{} {}'.format(
+            self.render_field_name_sql(field_name),
+            field_type
+        )
 
     def render_datatype_bool_sql(self, field_name, field, **kwargs):
         return 'BOOL'
@@ -981,10 +1087,14 @@ def render(self, schema, query, **kwargs):
         :param query: Query, the query to render
         :param **kwargs: named arguments
             placeholders: boolean, True if place holders should remain
-        :returns: string if placeholders is False, (string, list) if placeholders is True
+        :returns: str if placeholders is False, tuple[str, list] if
+            placeholders is True
         """
         sql, sql_args = self.render_sql(schema, query)
-        placeholders = kwargs.get("placeholders", kwargs.get("placeholder", False))
+        placeholders = kwargs.get(
+            "placeholders",
+            kwargs.get("placeholder", False)
+        )
 
         if not placeholders:
             for sql_arg in sql_args:
diff --git a/prom/interface/sqlite.py b/prom/interface/sqlite.py
index b9c867a..ba2563d 100644
--- a/prom/interface/sqlite.py
+++ b/prom/interface/sqlite.py
@@ -2,14 +2,15 @@
 """
 Bindings for SQLite
 
-https://docs.python.org/2/library/sqlite3.html
+https://docs.python.org/3/library/sqlite3.html
+https://github.com/omnilib/aiosqlite
 
 Notes, certain SQLite versions might have a problem with long integers
 http://jakegoulding.com/blog/2011/02/06/sqlite-64-bit-integers/
 
 Looking at the docs, it says it will set an integer value to 1-4, 6, or 8 bytes
-depending on the size, but I couldn't get it to accept anything over the 32-bit signed
-integer value of around 2billion
+depending on the size, but I couldn't get it to accept anything over the 32-bit
+signed integer value of around 2billion
 
 savepoints and transactions are similar to Postgres
 https://www.sqlite.org/lang_savepoint.html
@@ -21,7 +22,6 @@
 other links that were helpful
 http://www.numericalexpert.com/blog/sqlite_blob_time/
 """
-from __future__ import unicode_literals, division, print_function, absolute_import
 import os
 import decimal
 import datetime
@@ -32,6 +32,7 @@
 import uuid
 
 from datatypes import Datetime
+import aiosqlite
 
 # first party
 from ..exception import (
@@ -48,11 +49,10 @@
 from .sql import SQLInterface, SQLConnection
 
 
-class SQLiteConnection(SQLConnection, sqlite3.Connection):
-    """
-    Thin wrapper around the default connection to make sure it has a similar interface
-    to Postgres' connection instance so the common code can all be the same in the
-    parent class
+class AsyncSQLiteConnection(SQLConnection, aiosqlite.Connection):
+    """Thin wrapper around the default connection to make sure it has a similar
+    interface to Postgres' connection instance so the common code can all be the
+    same in the Interface class
 
     https://docs.python.org/3.11/library/sqlite3.html#sqlite3.Connection
     """
@@ -60,10 +60,16 @@ def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.closed = 0
 
-    def close(self, *args, **kwargs):
-        r = super().close(*args, **kwargs)
-        self.closed = 1
-        return r
+    async def close(self, *args, **kwargs):
+        try:
+            return await super().close(*args, **kwargs)
+
+        except ValueError:
+            # aiosqlite: ValueError: no active connection
+            pass
+
+        finally:
+            self.closed = 1
 
 
 class BooleanType(object):
@@ -76,7 +82,8 @@ def adapt(cls, val):
 
     @classmethod
     def convert(cls, val):
-        """from the db you get values like b'0' and b'1', convert those to True/False"""
+        """from the db you get values like b'0' and b'1', convert those to
+        True/False"""
         return bool(int(val))
 
 
@@ -90,14 +97,15 @@ def adapt(cls, val):
 
 
 class DatetimeType(StringType):
-    """External sqlite3 databases can store the TIMESTAMP type as unix timestamps,
-    this caused parsing problems when pulling the values out of the db because the
-    default adapter expected TIMESTAMP to be in the form of YYYY-MM-DD HH:MM:SS.SSSSSS
-    and so it would fail to convert the DDDDDD.DDD values, this handles that conversion
+    """External sqlite3 databases can store the TIMESTAMP type as unix
+    timestamps, this caused parsing problems when pulling the values out of the
+    db because the default adapter expected TIMESTAMP to be in the form of
+    YYYY-MM-DD HH:MM:SS.SSSSSS and so it would fail to convert the DDDDDD.DDD
+    values, this handles that conversion
 
     https://www.sqlite.org/lang_datefunc.html
-    the "unixepoch" modifier only works for dates between 0000-01-01 00:00:00 and
-    5352-11-01 10:52:47 (unix times of -62167219200 through 106751991167)
+    the "unixepoch" modifier only works for dates between 0000-01-01 00:00:00
+    and 5352-11-01 10:52:47 (unix times of -62167219200 through 106751991167)
 
     uses the name TIMESTAMP over DATETIME to be consistent with Postgres
     """
@@ -110,13 +118,13 @@ def adapt(cls, val):
     @classmethod
     def convert(cls, val):
         return Datetime(super().adapt(val)).datetime()
-        #return Datetime(super().adapt(val)).datetime().replace(tzinfo=datetime.timezone.utc)
 
 
 class NumericType(object):
     """numbers bigger than 64bit integers can be stored as this
 
-    This is named to be as consistent with Postgres's NUMERIC(<precision>, 0) type
+    This is named to be as consistent with Postgres's NUMERIC(<precision>, 0)
+    type
 
     This has TEXT in the name so it is treated as text according to SQLite's
     order of affinity rule 2:
@@ -138,7 +146,8 @@ def adapt(cls, val):
 
     @classmethod
     def convert(cls, val):
-        """This should only be called when the column type is actually FIELD_TYPE"""
+        """This should only be called when the column type is actually
+        FIELD_TYPE"""
         return int(val)
 
 
@@ -158,7 +167,8 @@ def convert(val):
 class DictType(object):
     """Converts a dict to json text and back again
 
-    Uses JSONBTEXT to be as close to Postgres while still triggering SQLite's affinity rule 2
+    Uses JSONBTEXT to be as close to Postgres while still triggering SQLite's
+    affinity rule 2
     """
     FIELD_TYPE = 'JSONBTEXT'
 
@@ -168,7 +178,8 @@ def adapt(cls, val):
 
     @classmethod
     def convert(cls, val):
-        """This should only be called when the column type is actually FIELD_TYPE"""
+        """This should only be called when the column type is actually
+        FIELD_TYPE"""
         return json.loads(val)
 
 
@@ -181,27 +192,29 @@ class SQLite(SQLInterface):
     _connection = None
 
     @classmethod
-    def configure(cls, config):
-        dsn = getattr(config, 'dsn', '')
-        if dsn:
-            host = config.host
-            db = config.database
-            if not host:
-                path = db
+    async def configure(cls, config):
+        if not config.get("path"):
+            dsn = config.get("dsn", "")
+            if dsn:
+                host = config.host
+                db = config.database
+                if not host:
+                    path = db
+
+                elif not db:
+                    path = host
 
-            elif not db:
-                path = host
+                else:
+                    path = os.sep.join([host, db])
 
             else:
-                path = os.sep.join([host, db])
+                path = config.database
 
-        else:
-            path = config.database
+            if not path:
+                raise ValueError("no sqlite db path found in config")
 
-        if not path:
-            raise ValueError("no sqlite db path found in config")
+            config.path = path
 
-        config.path = path
         return config
 
     def get_paramstyle(self):
@@ -210,10 +223,11 @@ def get_paramstyle(self):
         """
         return sqlite3.paramstyle
 
-    def _connect(self, config):
+    def _connector(self):
         """
         https://docs.python.org/3.11/library/sqlite3.html#sqlite3.connect
         """
+        config = self.config
         path = config.path
 
         # https://docs.python.org/2/library/sqlite3.html#default-adapters-and-converters
@@ -222,8 +236,8 @@ def _connect(self, config):
             #'isolation_level': "IMMEDIATE",
             #'isolation_level': "EXCLUSIVE",
             'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES,
-            'factory': SQLiteConnection,
-            'check_same_thread': True, # https://stackoverflow.com/a/2578401/5006
+            #'factory': SQLiteConnection,
+            'check_same_thread': True, # https://stackoverflow.com/a/2578401
             #'timeout': 100,
         }
         option_keys = list(options.keys()) + ['timeout', 'cached_statements']
@@ -232,8 +246,7 @@ def _connect(self, config):
                 options[k] = config.options[k]
 
         try:
-            self._connection = sqlite3.connect(path, **options)
-            self.log_debug(f"Connected to connection 0x{id(self._connection):02x}")
+            connection = sqlite3.connect(path, **options)
 
         except sqlite3.DatabaseError as e:
             path_d = os.path.dirname(path)
@@ -243,12 +256,18 @@ def _connect(self, config):
             else:
                 # let's try and make the directory path and connect again
                 dir_util.mkpath(path_d)
-                self._connection = sqlite3.connect(path, **options)
+                connection = sqlite3.connect(path, **options)
 
         # https://docs.python.org/2/library/sqlite3.html#row-objects
-        self._connection.row_factory = sqlite3.Row
+        connection.row_factory = sqlite3.Row
         # https://docs.python.org/2/library/sqlite3.html#sqlite3.Connection.text_factory
-        self._connection.text_factory = StringType.adapt
+        connection.text_factory = StringType.adapt
+
+        # NOTE -- it's bad encapsulation that these are saved on the module,
+        # Psycopg3 allows these adapters to be placed on the connection instead
+        # of the moduel and I like that a lot better since once prom is
+        # connected the first time then raw sqlite3 is basically borked for the
+        # life of the script
 
         # for some reason this is needed in python 3.6 in order for saved bytes
         # to be ran through the converter, not sure why
@@ -261,7 +280,10 @@ def _connect(self, config):
         # has to be adapted even if its parent is already registered
         sqlite3.register_adapter(datetime.datetime, DatetimeType.adapt)
         sqlite3.register_adapter(Datetime, DatetimeType.adapt)
-        sqlite3.register_converter(DatetimeType.FIELD_TYPE, DatetimeType.convert)
+        sqlite3.register_converter(
+            DatetimeType.FIELD_TYPE,
+            DatetimeType.convert
+        )
 
         sqlite3.register_adapter(int, NumericType.adapt)
         sqlite3.register_converter(NumericType.FIELD_TYPE, NumericType.convert)
@@ -272,32 +294,45 @@ def _connect(self, config):
         sqlite3.register_adapter(dict, DictType.adapt)
         sqlite3.register_converter(DictType.FIELD_TYPE, DictType.convert)
 
-    def _configure_connection(self, **kwargs):
+        return connection
+
+    async def _connect(self, config):
+        self._connection = AsyncSQLiteConnection(
+            self._connector,
+            iter_chunk_size=config.options.get("iter_chunk_size", 64)
+        )
+        self._connection.start()
+        await self._connection._connect()
+
+        self.log_debug("Connected to connection {}", self._connection)
+
+        await self.configure_connection(connection=self._connection)
+
+    async def _configure_connection(self, **kwargs):
         # turn on foreign keys
         # http://www.sqlite.org/foreignkeys.html
-        self._raw('PRAGMA foreign_keys = ON', ignore_result=True, **kwargs)
-
-        # by default we can read/write, so only bother to run this query if we need to
-        # make the connection actually readonly
-        if self.config.readonly:
-            self._readonly(self.config.readonly, **kwargs)
+        await self._raw(
+            "PRAGMA foreign_keys = ON",
+            ignore_result=True,
+            **kwargs
+        )
 
-    def _get_connection(self):
+    async def _get_connection(self):
         return self._connection
 
-    def _close(self):
-        self._connection.close()
+    async def _close(self):
+        await self._connection.close()
         self._connection = None
 
-    def _readonly(self, readonly, **kwargs):
-        self._raw(
+    async def _readonly(self, readonly, **kwargs):
+        await self._raw(
             # https://stackoverflow.com/a/49630725/5006
             'PRAGMA query_only = {}'.format("ON" if readonly else "OFF"),
             ignore_result=True,
             **kwargs
         )
 
-    def _get_tables(self, table_name, **kwargs):
+    async def _get_tables(self, table_name, **kwargs):
         query_str = 'SELECT tbl_name FROM sqlite_master WHERE type = ?'
         query_args = ['table']
 
@@ -305,51 +340,40 @@ def _get_tables(self, table_name, **kwargs):
             query_str += ' AND name = ?'
             query_args.append(str(table_name))
 
-        ret = self._raw(query_str, *query_args, **kwargs)
+        ret = await self._raw(query_str, *query_args, **kwargs)
         return [r['tbl_name'] for r in ret]
 
-    def _get_indexes(self, schema, **kwargs):
+    async def _get_indexes(self, schema, **kwargs):
         """return all the indexes for the given schema"""
         # http://www.sqlite.org/pragma.html#schema
         # http://www.mail-archive.com/sqlite-users@sqlite.org/msg22055.html
         # http://stackoverflow.com/questions/604939/
         ret = {}
-        rs = self._raw('PRAGMA index_list({})'.format(self.render_table_name_sql(schema)), **kwargs)
+        rs = await self._raw(
+            'PRAGMA index_list({})'.format(self.render_table_name_sql(schema)),
+            **kwargs
+        )
         if rs:
             for r in rs:
                 iname = r['name']
                 ret.setdefault(iname, [])
-                indexes = self._raw('PRAGMA index_info({})'.format(r['name']), **kwargs)
+                indexes = await self._raw(
+                    'PRAGMA index_info({})'.format(r['name']),
+                    **kwargs
+                )
                 for idict in indexes:
                     ret[iname].append(idict['name'])
 
         return ret
 
-    def _delete_tables(self, **kwargs):
-        self._raw('PRAGMA foreign_keys = OFF', ignore_result=True, **kwargs);
-        ret = super()._delete_tables(**kwargs)
-        self._raw('PRAGMA foreign_keys = ON', ignore_result=True, **kwargs);
-        return ret
-
-    def _delete_table(self, schema, **kwargs):
+    async def _delete_table(self, schema, **kwargs):
         """
         https://www.sqlite.org/lang_droptable.html
         """
-        #query_str = 'DROP TABLE IF EXISTS {}'.format(str(schema))
-        query_str = "DROP TABLE IF EXISTS {}".format(self.render_table_name_sql(schema))
-        self._raw(query_str, ignore_result=True, **kwargs)
-
-    def _inserts(self, schema, field_names, field_values, **kwargs):
-        """
-        https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executemany
-        """
-        query_str = self.render_inserts_sql(schema, field_names, **kwargs)
-        with self.connection(**kwargs) as connection:
-            cur = connection.cursor()
-            cur.executemany(
-                query_str,
-                field_values,
-            )
+        query_str = "DROP TABLE IF EXISTS {}".format(
+            self.render_table_name_sql(schema)
+        )
+        await self._raw(query_str, ignore_result=True, **kwargs)
 
     def create_error(self, e, **kwargs):
         kwargs.setdefault("error_module", sqlite3)
@@ -381,23 +405,35 @@ def create_error(self, e, **kwargs):
                 e = CloseError(e)
 
             elif "Incorrect number of bindings supplied" in e_msg:
-                e = PlaceholderError(e, *kwargs.get("error_args", []))
+                e = PlaceholderError(e)
 
             else:
                 e = super().create_error(e, **kwargs)
 
         elif isinstance(e, sqlite3.InterfaceError):
             e_msg = str(e)
-            if "Error binding parameter" in e_msg and "error_args" in kwargs:
-                error_args = kwargs["error_args"]
-                ms = re.search(r"parameter\s(\d+)", e_msg)
-                index = int(ms.group(1))
-                value = error_args[1][index]
-                e = PlaceholderError(
-                    e,
-                    *error_args,
-                    message=f"Placeholder {index} of query has unexpected type {type(value)}",
-                )
+            if "Error binding parameter" in e_msg:
+                if error_args := kwargs.get("error_args", []):
+                    ms = re.search(r"parameter\s(\d+)", e_msg)
+                    index = int(ms.group(1))
+                    value = error_args[1][index]
+                    msg = "Query Placeholder {} has unexpected type {}".format(
+                        index,
+                        type(value)
+                    )
+
+                    e = PlaceholderError(e, message=msg)
+
+                else:
+                    e = PlaceholderError(e)
+
+            else:
+                e = super().create_error(e, **kwargs)
+
+        elif isinstance(e, ValueError):
+            e_msg = str(e)
+            if "no active connection" in e_msg: # aiosqlite specific
+                e = CloseError(e)
 
             else:
                 e = super().create_error(e, **kwargs)
@@ -407,16 +443,18 @@ def create_error(self, e, **kwargs):
 
         return e
 
-    def _get_fields(self, table_name, **kwargs):
+    async def _get_fields(self, table_name, **kwargs):
         """return all the fields for the given table"""
         ret = {}
-        query_str = 'PRAGMA table_info({})'.format(self.render_table_name_sql(table_name))
-        fields = self._raw(query_str, **kwargs)
-        #pout.v([dict(d) for d in fields])
+        query_str = 'PRAGMA table_info({})'.format(
+            self.render_table_name_sql(table_name)
+        )
+        fields = await self._raw(query_str, **kwargs)
 
-        query_str = 'PRAGMA foreign_key_list({})'.format(self.render_table_name_sql(table_name))
-        fks = {f["from"]: f for f in self._raw(query_str, **kwargs)}
-        #pout.v([dict(d) for d in fks.values()])
+        query_str = 'PRAGMA foreign_key_list({})'.format(
+            self.render_table_name_sql(table_name)
+        )
+        fks = {f["from"]: f for f in await self._raw(query_str, **kwargs)}
 
         pg_types = {
             "INTEGER": int,
@@ -439,8 +477,9 @@ def _get_fields(self, table_name, **kwargs):
                 if adapter_class.FIELD_TYPE not in pg_types:
                     pg_types[adapter_class.FIELD_TYPE] = field_type[0]
 
-        # the rows we can set: field_type, name, field_required, min_size, max_size,
-        #   size, unique, pk, <foreign key info>
+        # the rows we can set:
+        #   field_type, name, field_required, min_size, max_size, size, unique,
+        #   pk, <foreign key info>
         for row in fields:
             field = {
                 "name": row["name"],
@@ -475,13 +514,17 @@ def render_date_field_sql(self, field_name, field_kwargs, symbol):
             'julian_day': "strftime('%J', {})", # YYYY-MM-DD
             'month': "CAST(strftime('%m', {}) AS integer)",
             'minute': "CAST(strftime('%M', {}) AS integer)",
-            'dow': "CAST(strftime('%w', {}) AS integer)", # day of week 0 = sunday
+            'dow': "CAST(strftime('%w', {}) AS integer)", # day of week 0=sunday
             'week': "CAST(strftime('%W', {}) AS integer)",
             'year': "CAST(strftime('%Y', {}) AS integer)"
         }
 
         for k, v in field_kwargs.items():
-            fstrs.append([k_opts[k].format(self.render_field_name_sql(field_name)), self.PLACEHOLDER, v])
+            fstrs.append([k_opts[k].format(
+                self.render_field_name_sql(field_name)),
+                self.PLACEHOLDER,
+                v
+            ])
 
         return fstrs
 
@@ -498,10 +541,15 @@ def render_sort_field_sql(self, field_name, field_vals, sort_dir_str):
         else:
             fvi = (t for t in enumerate(reversed(field_vals))) 
 
-        query_sort_str = ['  CASE {}'.format(self.render_field_name_sql(field_name))]
+        query_sort_str = ['  CASE {}'.format(
+            self.render_field_name_sql(field_name)
+        )]
         query_args = []
         for i, v in fvi:
-            query_sort_str.append('    WHEN {} THEN {}'.format(self.PLACEHOLDER, i))
+            query_sort_str.append('    WHEN {} THEN {}'.format(
+                self.PLACEHOLDER,
+                i
+            ))
             query_args.append(v)
 
         query_sort_str.append('  END')
@@ -527,7 +575,11 @@ def render_datatype_int_sql(self, field_name, field, **kwargs):
         return field_type
 
     def render_datatype_str_sql(self, field_name, field, **kwargs):
-            field_type = super().render_datatype_str_sql(field_name, field, **kwargs)
+            field_type = super().render_datatype_str_sql(
+                field_name,
+                field,
+                **kwargs
+            )
 
             fo = field.interface_options
             if fo.get('ignore_case', False):
diff --git a/prom/model.py b/prom/model.py
index ac74826..9d15911 100644
--- a/prom/model.py
+++ b/prom/model.py
@@ -1,59 +1,26 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import inspect
-import sys
-import datetime
 
 from datatypes import (
-    Pool,
     EnglishWord,
     NamingConvention,
     classproperty
 )
 
-# first party
+from .compat import *
 from .query import Query, Iterator
 from . import utils
 from .interface import get_interface
 from .config import (
     Schema,
-    Field,
     AutoDatetime,
     AutoIncrement,
-    Index
 )
-from .compat import *
-
-
-class OrmPool(Pool):
-    """
-    Create a pool of Orm instances, which is just a dict of primary_key -> Orm instance
-    mappings
-
-    Let's say you are iterating through millions of rows of Foo, and for each Foo
-    instance you need to get the Bar instance from the Foo.bar_id field, and lots of
-    Foos have the same bar_id, but you only want to pull the Bar instance from
-    the db once, this allows you to easily do that
-
-    :Example:
-        bar_pool = Bar.pool(500) # keep the pool contained to the last 500 Bar instances
-        for f in Foo.query.all():
-            b = bar_pool[f.bar_id]
-            print "Foo {} loves Bar {}".format(f.pk, b.pk)
-    """
-    def __init__(self, orm_class, maxsize=0):
-        super(OrmPool, self).__init__(maxsize=maxsize)
-        self.orm_class = orm_class
-
-    def __missing__(self, pk):
-        o = self.orm_class.query.eq_pk(pk).one()
-        self[pk] = o
-        return o
 
 
 class Orm(object):
     """
-    this is the parent class of any Orm child class you want to create that can access the db
+    this is the parent class of any Orm child class you want to create that can
+    access the db
 
     :example:
         from prom import Orm, Field, Index
@@ -68,24 +35,60 @@ class Foo(Orm):
             index_barche = Index('bar', 'che')
     """
     connection_name = ""
-    """the name of the connection to use to retrieve the interface"""
+    """the name of the connection to use to retrieve the interface
+
+    In your DSNs you can set a connection name using the fragment, then the
+    Interface instance that matches fragment and .connection_name will be used
+    by this orm to interact with the db
+    """
 
     query_class = Query
-    """the class this Orm will use to create Query instances to query the db"""
+    """the class this Orm will use to create Query instances to query the db
+
+    You can override this in your child class and you create new instances by
+    calling .query
+    """
 
     iterator_class = Iterator
-    """the class this Orm will use for iterating through results returned from db"""
+    """the class this Orm will use for iterating through results returned from
+    db
+
+    This is returned from the Query.get
+    """
 
     orm_classes = {}
     """This will hold all other orm classes that have been loaded into memory
     the class path is the key and the class object is the value"""
 
     _id = AutoIncrement(aliases=["id"])
+    """The primary key is an auto-increment integer by default
+
+    You can override this in your child class if you want a different primary
+    key, I wouldn't change the name though. The _id name comes from MongoDB
+    """
+
     _created = AutoDatetime(created=True, updated=False, aliases=["created"])
+    """Anytime a new row is created this will be populated
+
+    If you don't want this functionality just do `_created = None` in your
+    child class
+    """
+
     _updated = AutoDatetime(created=False, updated=True, aliases=["updated"])
+    """Anytime a row is written to this will be updated, that means it will have
+    roughly the same value as ._created when the row is first inserted
+
+    If you don't want this functionality just do `_updated = None` in your
+    child class
+    """
 
     @classproperty
     def table_name(cls):
+        """The name of the table this orm is wrapping
+
+        To be more explicit it is nice to override this attribute in your child
+        classes, but it's nice to have a default value when rapidly prototyping
+        """
         return NamingConvention("{}_{}".format(
             cls.__module__.replace(".", "_"),
             cls.__name__
@@ -117,7 +120,12 @@ def models_name(cls):
 
     @classproperty
     def schema(cls):
-        """the Schema() instance that this class will derive all its db info from"""
+        """the Schema instance that this class will derive all its db info from
+
+        Unless you really know what you are doing you should never have to set
+        this value, it will be automatically created using the Field instances
+        you define on your child class
+        """
         return Schema.get_instance(cls)
 
     @classproperty
@@ -125,41 +133,53 @@ def interface(cls):
         """
         return an Interface instance that can be used to access the db
 
-        return -- Interface() -- the interface instance this Orm will use
+        :returns: Interface, the interface instance this Orm will use
         """
         return get_interface(cls.connection_name)
 
     @classproperty
     def query(cls):
-        """return a new Query instance ready to make a db call using the child class
+        """return a new Query instance ready to make a db call using the child
+        class
 
         :example:
             # fluid interface
-            results = Orm.query.is_foo('value').desc_bar().get()
+            results = await Orm.query.is_foo('value').desc_bar().get()
 
-        :returns: Query, every time this is called a new query instance is created using cls.query_class
+        :returns: Query, every time this is called a new query instance is
+            created using the .query_class attribute
         """
         query_class = cls.query_class
         return query_class(orm_class=cls)
 
     @property
     def field_names(self):
-        """Return all the field names"""
+        """Return all the field names
+
+        :returns: list[str], the field names
+        """
         return [k for k in self.schema.fields]
 
     @property
     def fields(self):
-        """
-        return all the fields and their raw values for this Orm instance. This
-        property returns a dict with the field names and their current values
+        """return all the fields and their raw values for this Orm instance.
+        This property returns a dict with the field names and their current
+        values
+
+        if you want to control the values for outputting to an api, use
+        .jsonable() instead
 
-        if you want to control the values for outputting to an api, use .jsonable()
+        :returns: dict[str, Any]
         """
         return {k:getattr(self, k, None) for k in self.schema.fields}
 
     @property
     def modified_field_names(self):
-        """Return all the field names that are currently modified"""
+        """Return all the field names that are currently modified
+
+        :returns: set[str], all the fields that are currently considered 
+            modified on this instance
+        """
         modified_field_names = set()
         for field_name, field in self.schema.fields.items():
             if field.modified(self, getattr(self, field_name)):
@@ -168,59 +188,56 @@ def modified_field_names(self):
 
     @property
     def modified_fields(self):
-        """Return a dict of field_names/field_values for all the currently modified fields"""
-        return {k:getattr(self, k) for k in self.modified_field_names}
+        """Return a dict of field_names/field_values for all the currently
+        modified fields
 
-    @classmethod
-    def pool(cls, maxsize=0):
+        :returns: dict[str, Any]
         """
-        return a new OrmPool instance
-
-        return -- OrmPool -- the orm pool instance will be tied to this Orm
-        """
-        return OrmPool(orm_class=cls, maxsize=maxsize)
+        return {k:getattr(self, k) for k in self.modified_field_names}
 
     @classmethod
-    def transaction(cls, **kwargs):
+    async def transaction(cls, **kwargs):
         """Create a transaction for this Orm
 
         :Example:
-            with FooOrm.transaction() as conn:
+            async with FooOrm.transaction() as conn:
                 o = FooOrm(foo=1)
                 o.save(connection=conn)
 
-        :param **kwargs: passed through to the Interface.transaction context manager
+        :param **kwargs: passed through to the Interface.transaction
             * prefix: str, the name of the transaction you want to use
-            * nest: bool, True if you want nested transactions to be created, False
-                to ignore nested transactions
-        :returns: Connection instance
+            * nest: bool, True if you want nested transactions to be created,
+                False to ignore nested transactions
+        :returns: Connection, the connection instance with an active tx
         """
         kwargs.setdefault("nest", False)
         kwargs.setdefault("prefix", f"{cls.__name__}_{cls.connection_name}_tx")
         return cls.interface.transaction(**kwargs)
 
     @classmethod
-    def create(cls, *args, **kwargs):
+    async def create(cls, *args, **kwargs):
         """
-        create an instance of cls with the passed in fields and set it into the db
+        create an instance of cls with the passed in fields and set it into the
+        db
 
-        this method takes *args, **kwargs because a child class can override .__init__
-        and it's nice to not have to modify this method also
+        this method takes *args, **kwargs because a child class can override
+        .__init__ and it's nice to not have to modify this method also
 
         :param *args: passed directly to .__init__
         :param **kwargs: passed directly to .__init__
         :returns: Orm instance that has been saved into the db
         """
-        # NOTE -- you cannot use hydrate/populate here because populate alters modified fields
+        # NOTE -- you cannot use hydrate/populate here because populate alters
+        # modified fields
         connection = kwargs.pop("connection", None)
 
         instance = cls(*args, **kwargs)
 
         if connection:
-            instance.save(connection=connection)
+            await instance.save(connection=connection)
 
         else:
-            instance.save()
+            await instance.save()
 
         return instance
 
@@ -228,11 +245,12 @@ def create(cls, *args, **kwargs):
     def hydrate(cls, fields=None, **fields_kwargs):
         """return a populated instance with the present fields
 
-        NOTE -- you probably shouldn't override this method since the Query methods
-        rely on this method signature to create each instance of the results
+        NOTE -- you probably shouldn't override this method since 
+        Iterator.hydrate relies on this method signature to create each instance
 
         :param fields: dict, the fields to populate in this instance
-        :param **fields_kwargs: dict, the fields in key=val form to populate in this instance
+        :param **fields_kwargs: dict, the fields in key=val form to populate in
+            this instance
         :returns: an instance of this class with populated fields
         """
         instance = cls()
@@ -243,14 +261,16 @@ def hydrate(cls, fields=None, **fields_kwargs):
 
     @classmethod
     def make_dict(cls, fields, fields_kwargs, schema=None):
-        """Lots of methods take a dict and key=val for fields, this combines fields
-        and fields_kwargs into one master dict, turns out we want to do this more
-        than I would've thought to keep api compatibility with prom proper
+        """Lots of methods take a dict and key=val for fields, this combines
+        fields and fields_kwargs into one master dict, turns out we want to do
+        this more than I would've thought to keep api compatibility with prom
+        proper
 
         :param fields: dict, the fields in a dict
-        :param fields_kwargs: dict, if you would like to pass the fields as key=val
-            this picks those up and combines them with fields
-        :schema: Schema, if passed in then this will normalize field names
+        :param fields_kwargs: dict, if you would like to pass the fields as
+            key=val this picks those up and combines them with fields
+        :schema: Schema, if passed in then this will normalize field names and
+            resolve any aliases
         :returns: dict, the combined fields
         """
         fields = utils.make_dict(fields, fields_kwargs)
@@ -270,8 +290,12 @@ def __init__(self, fields=None, **fields_kwargs):
         also need to override .hydrate (but don't change .hydrate's signature)
         since .hydrate creates an instance using no arguments
 
+        NOTE -- Honestly, I've tried it multiple times and it's almost never
+            worth overriding this method nor .hydrate. If you ever get tempted
+            just say no!
+
         :param fields: dict, the fields in a dict
-        :param **fields_kwargs: dict, if you would like to pass the fields as key=val
+        :param **fields_kwargs: if you would like to pass the fields as key=val
         """
         self._interface_pk = None
         self._interface_hydrate = False
@@ -281,21 +305,26 @@ def __init__(self, fields=None, **fields_kwargs):
 
         # set defaults
         for field_name, field in schema.fields.items():
-            fields[field_name] = field.fdefault(self, fields.get(field_name, None))
+            fields[field_name] = field.fdefault(
+                self,
+                fields.get(field_name, None)
+            )
 
         self.modify(fields)
 
     def __init_subclass__(cls):
-        """When this class is loaded into memory it will be saved into cls.orm_classes,
-        this way every orm class knows about all the others
+        """When a child class is loaded into memory it will be saved into
+        .orm_classes, this way every orm class knows about all the other orm
+        classes, this is the method that makes that possible magically
 
         https://peps.python.org/pep-0487/
         """
         super().__init_subclass__()
-        cls.orm_classes[f"{cls.__module__}:{cls.__name__}"] = cls
+        cls.orm_classes[f"{cls.__module__}:{cls.__qualname__}"] = cls
 
     def fk(self, orm_class):
-        """find the field value in self that is the primary key of the passed in orm_class
+        """find the field value in self that is the primary key of the passed in
+        orm_class
 
         :example:
             class Foo(Orm):
@@ -307,29 +336,34 @@ class Bar(Orm):
             b = Bar(foo_id=1)
             print(b.fk(Foo)) # 1
 
-        :param orm_class: Orm, the fields in self will be checked until the field that
-            references Orm is found, then the value of that field will be returned
-        :returns: the self field value that is a foreign key references to orm_class
+        :param orm_class: Orm, the fields in self will be checked until the
+            field that references Orm is found, then the value of that field
+            will be returned
+        :returns: the self field value that is a foreign key references to
+            orm_class
         """
         for field_name, field in self.schema.ref_fields.items():
             if field.schema is orm_class.schema:
                 return getattr(self, field_name)
 
-        raise ValueError("Did not find a foreign key reference for {} in {}".format(
-            orm_class.__name__,
-            self.__class__.__name__,
-        ))
+        raise ValueError(
+            "Did not find a foreign key reference for {} in {}".format(
+                orm_class.__name__,
+                self.__class__.__name__,
+            )
+        )
 
     def ref(self, orm_classpath):
         """see Query.ref() for an explanation of what this method does
 
-        :param orm_classpath: string|type, a full python class path (eg, foo.bar.Che) or
-            an actual model.Orm python class
+        :param orm_classpath: string|type, a full python class path (eg,
+            foo.bar.Che) or an actual model.Orm python class
         :returns: Orm
         """
         return self.query.ref(orm_classpath).orm_class
 
     def ref_class(self, orm_classpath):
+        """Alias for .ref to be more consistent with other *_class attributes"""
         return self.ref(orm_classpath)
 
     def is_hydrated(self):
@@ -361,7 +395,7 @@ def from_interface(self, fields):
         self._interface_pk = self.pk
 
     def to_interface(self):
-        """Get all the fields that need to be saved
+        """Get all the fields that need to be persisted into the db
 
         :returns: dict[str, Any], key is field_name and val is the field value
             to be saved
@@ -374,22 +408,31 @@ def to_interface(self):
             is_modified = field.modified(self, v)
             if is_modified:
                 if not field.options.get("empty", True) and not v:
-                    raise ValueError(f"{self.__class__.__name__}.{k} cannot be empty")
+                    raise ValueError(
+                        f"{self.__class__.__name__}.{k} cannot be empty"
+                    )
 
                 fields[k] = v
 
             if v is None and field.is_required():
                 if field.is_pk():
                     if is_modified:
-                        raise KeyError("Primary key has been removed and is required")
+                        raise KeyError(
+                            "Primary key has been removed and is required"
+                        )
 
                 else:
                     if self.is_insert() or is_modified:
-                        raise KeyError(f"Missing required field {self.__class__.__name__}.{k}")
+                        raise KeyError(
+                            "Missing required field {}.{}".format(
+                                self.__class__.__name__,
+                                k
+                            )
+                        )
 
         return fields
 
-    def insert(self, **kwargs):
+    async def insert(self, **kwargs):
         """persist the field values of this orm"""
         ret = True
 
@@ -398,7 +441,7 @@ def insert(self, **kwargs):
 
         q = self.query
         q.set(fields)
-        pk = q.insert(**kwargs)
+        pk = await q.insert(**kwargs)
         if pk:
             fields = q.fields_set.fields
             pk_name = schema.pk_name
@@ -411,8 +454,9 @@ def insert(self, **kwargs):
 
         return ret
 
-    def update(self, **kwargs):
-        """re-persist the updated field values of this orm that has a primary key"""
+    async def update(self, **kwargs):
+        """re-persist the updated field values of this orm that has a primary
+        key"""
         ret = True
         fields = self.to_interface()
 
@@ -426,7 +470,7 @@ def update(self, **kwargs):
         else:
             raise ValueError("Cannot update an unhydrated orm instance")
 
-        if q.update(**kwargs):
+        if await q.update(**kwargs):
             fields = q.fields_set.fields
             self.from_interface(fields)
 
@@ -435,25 +479,25 @@ def update(self, **kwargs):
 
         return ret
 
-    def upsert(self, **kwargs):
-        """Perform an UPSERT query where we insert the fields if they don't already
-        exist on the db or we UPDATE if they do
+    async def upsert(self, **kwargs):
+        """Perform an UPSERT query where we insert the fields if they don't
+        already exist on the db or we UPDATE if they do
 
         We only want to upsert on specific occasions where we know we've set the
         conflict values and will be sending them to the db. UPSERT queries need
-        to have a unique index on the table they can use for the conflict fields.
+        to have a unique index on the table they can use for the conflict fields
 
         This method will go through the indexes and try and find a unique index
         that has all fields that are being sent to the interface and it will use
-        those fields as the conflict fields, it will raise a ValueError if it can't
-        find a valid set of conflict fields
+        those fields as the conflict fields, it will raise a ValueError if it
+        can't find a valid set of conflict fields
 
         :param **kwargs: passed through to the interface
         """
         ret = True
 
         if pk := self._interface_pk:
-            ret = self.update()
+            ret = await self.update(**kwargs)
 
         else:
             schema = self.schema
@@ -462,15 +506,14 @@ def upsert(self, **kwargs):
             conflict_fields = self.conflict_fields(fields)
             if not conflict_fields:
                 raise ValueError(
-                    "{}.upsert() failed to find conflict field names from: {}".format(
-                        self.__class__.__name__,
+                    "Failed to find conflict field names from: {}".format(
                         list(fields.keys())
                     )
                 )
 
             q = self.query
             q.set(fields)
-            pk = q.upsert([t[0] for t in conflict_fields], **kwargs)
+            pk = await q.upsert([t[0] for t in conflict_fields], **kwargs)
             if pk:
                 fields = q.fields_set.fields
                 pk_name = schema.pk_name
@@ -483,10 +526,9 @@ def upsert(self, **kwargs):
 
         return ret
 
-    def save(self, **kwargs):
-        """
-        persist the fields in this object into the db, this will update if _id is set, otherwise
-        it will insert
+    async def save(self, **kwargs):
+        """persist the fields in this object into the db, this will update if
+        _id is set, otherwise it will insert
 
         see also -- .insert(), .update()
         """
@@ -494,23 +536,27 @@ def save(self, **kwargs):
 
         pk = self._interface_pk
         if pk:
-            ret = self.update(**kwargs)
+            ret = await self.update(**kwargs)
         else:
-            ret = self.insert(**kwargs)
+            ret = await self.insert(**kwargs)
 
         return ret
 
-    def delete(self, **kwargs):
+    async def delete(self, **kwargs):
         """delete the object from the db if pk is set"""
         ret = False
         q = self.query
         pk = self._interface_pk
         if pk:
             pk_name = self.schema.pk.name
-            self.query.eq_field(pk_name, pk).delete(**kwargs)
+            await self.query.eq_field(pk_name, pk).delete(**kwargs)
 
             for field_name, field in self.schema.fields.items():
-                setattr(self, field_name, field.idel(self, getattr(self, field_name)))
+                setattr(
+                    self,
+                    field_name,
+                    field.idel(self, getattr(self, field_name))
+                )
 
             self._interface_pk = None
             self._interface_hydrate = False
@@ -520,7 +566,8 @@ def delete(self, **kwargs):
         return ret
 
     def conflict_fields(self, fields):
-        """Internal method. This will find fields that can be used for .upsert/.load
+        """Internal method. This will find fields that can be used for
+        .upsert/.load
 
         :param fields: dict, the fields to check for values that would satisfy
             unique indexes or a primary key
@@ -545,7 +592,9 @@ def conflict_fields(self, fields):
                 if index.unique:
                     for field_name in index.field_names:
                         if field_name in fields:
-                            conflict_fields.append((field_name, fields[field_name]))
+                            conflict_fields.append(
+                                (field_name, fields[field_name])
+                            )
 
                         else:
                             conflict_fields = []
@@ -556,7 +605,7 @@ def conflict_fields(self, fields):
 
         return conflict_fields
 
-    def load(self):
+    async def load(self):
         """Given a partially populated orm try and load any missing fields from
         the db
 
@@ -565,7 +614,7 @@ def load(self):
         fields = self.modified_fields
         conflict_fields = self.conflict_fields(fields)
         if not conflict_fields:
-            raise ValueError(f"Load failed to find suitable fields to query on")
+            raise ValueError("Load failed to find suitable fields to query on")
 
         q = self.query
         for field_name, field_val in conflict_fields:
@@ -577,7 +626,7 @@ def load(self):
                 field_names.append(field_name)
 
         q.select(*field_names)
-        field_values = q.one()
+        field_values = await q.one()
         if field_values:
             ret = True
             fields = dict(zip(field_names, field_values))
@@ -591,9 +640,9 @@ def load(self):
 
         return ret
 
-    def requery(self):
-        """Fetch this orm from the db again (ie, re-query the row from the db and
-        return a new Orm instance with the columns from that row)"""
+    async def requery(self):
+        """Fetch this orm from the db again (ie, re-query the row from the db
+        and return a new Orm instance with the columns from that row)"""
         fields = {k:v for k, v in self.fields.items() if v is not None}
 
         conflict_fields = self.conflict_fields(fields)
@@ -604,29 +653,33 @@ def requery(self):
         for field_name, field_val in conflict_fields:
             q.eq_field(field_name, field_val)
 
-        return q.one()
+        return await q.one()
 
     def is_modified(self, field_name=""):
-        """true if a field, or any field, has been changed from its original value, false otherwise
+        """true if a field, or any field, has been changed from its original
+        value, false otherwise
 
-        :param field_name: string, the name of the field you want to check for modification
+        :param field_name: string, the name of the field you want to check for
+            modification
         :returns: bool
         """
         if field_name:
             ret = field_name in self.modified_field_names
+
         else:
             ret = len(self.modified_field_names) > 0
+
         return ret
 
     def modify(self, fields=None, **fields_kwargs):
-        """update the fields of this instance with the values in dict fields
+        """update the fields of this instance with the passed in values
 
         this should rarely be messed with, if you would like to manipulate the
-        fields you should override modify_fields()
+        fields you should override .modify_fields
 
         :param fields: dict, the fields in a dict
-        :param **fields_kwargs: dict, if you would like to pass the fields as key=val
-            this picks those up and combines them with fields
+        :param **fields_kwargs: dict, if you would like to pass the fields as
+            key=val this picks those up and combines them with fields
         """
         fields = self.make_dict(fields, fields_kwargs)
         fields = self.modify_fields(fields)
@@ -647,31 +700,54 @@ def modify_fields(self, fields):
         return fields
 
     def __getattr__(self, k):
+        """
+        NOTE -- this is a hybrid method, sometimes it will return coroutines
+        """
         ret = None
         try:
             field_name = self.schema.field_name(k)
 
         except AttributeError:
+            # Go through looking for a FK's model name, if a match is found then
+            # load that FK's row using this orm's field value and return an
+            # orm instance where this row's orm field value is the primary key
             for ref_field_name, ref_field in self.schema.ref_fields.items():
                 ref_class = ref_field.ref
                 if k == ref_class.model_name:
                     ref_field_value = getattr(self, ref_field_name, None)
-                    ret = None
+
                     if ref_field_value:
+                        # this is a coroutine
                         ret = ref_class.query.eq_pk(ref_field_value).one()
 
+                    else:
+                        # we do this so if there isn't a value it can still be
+                        # awaited and return None
+                        async def await_none(): return None
+                        ret = await_none()
+
                     return ret
 
-            # Go through all the orm_classes looking for a model_name or models_name
-            # match and query that model using that model's FK field name that
-            # matches self.pk
+            # Go through all the orm_classes looking for a model_name or
+            # models_name match and query that model using that model's FK field
+            # name that matches self.pk
             for orm_class in self.orm_classes.values():
                 if k == orm_class.models_name or k == orm_class.model_name:
-                    for ref_field_name, ref_field in orm_class.schema.ref_fields.items():
+                    ref_items = orm_class.schema.ref_fields.items()
+                    for ref_field_name, ref_field in ref_items:
                         ref_class = ref_field.ref
                         if ref_class and isinstance(self, ref_class):
-                            query = orm_class.query.eq_field(ref_field_name, self.pk)
-                            return query.all() if k == orm_class.models_name else query.one()
+                            query = orm_class.query.eq_field(
+                                ref_field_name,
+                                self.pk
+                            )
+                            if k == orm_class.models_name:
+                                # this is a coroutine
+                                return query.get()
+
+                            else:
+                                # this is a coroutine
+                                return query.one()
 
             raise
 
@@ -698,38 +774,37 @@ def __setattr__(self, k, v):
     def __delattr__(self, k):
         try:
             field_name = self.schema.field_name(k)
+
         except AttributeError:
             field_name = k
+
         return super(Orm, self).__delattr__(field_name)
 
     def __int__(self):
+        """Syntactic sugar to get the primary key as an int"""
         return int(self.pk)
 
-    def __long__(self):
-        return long(self.pk)
-
     def __str__(self):
-        #return self.__unicode__() if is_py2 else self.__bytes__()
+        """Syntactic sugar to get the primary key as a string"""
         return str(self.pk)
 
-    def __unicode__(self):
-        return unicode(self.pk)
-
     def __bytes__(self):
+        """Syntactic sugar to get the primary key as bytes"""
         return bytes(self.pk)
 
     def jsonable(self, *args, **options):
         """
         return a public version of this instance that can be jsonified
 
-        Note that this does not return _id, _created, _updated, the reason why is
-        because lots of times you have a different name for _id (like if it is a 
-        user object, then you might want to call it user_id instead of _id) and I
-        didn't want to make assumptions
+        Note that this does not return _id, _created, _updated, the reason why
+        is because lots of times you have a different name for _id (like if it
+        is a user object, then you might want to call it user_id instead of _id)
+        and I didn't want to make assumptions
 
-        note 2, I'm not crazy about the name, but I didn't like to_dict() and pretty
-        much any time I need to convert the object to a dict is for json, I kind of
-        like dictify() though, but I've already used this method in so many places
+        note 2, I'm not crazy about the name, but I didn't like to_dict() and
+        pretty much any time I need to convert the object to a dict is for json,
+        I kind of like dictify() though, but I've already used this method in so
+        many places. Another name I don't mind is .tojson
         """
         d = {}
         for field_name, field in self.schema.fields.items():
@@ -744,7 +819,7 @@ def jsonable(self, *args, **options):
         return d
 
     @classmethod
-    def install(cls):
+    async def install(cls):
         """install the Orm's table using the Orm's schema"""
-        return cls.interface.set_table(cls.schema)
+        return await cls.interface.set_table(cls.schema)
 
diff --git a/prom/query.py b/prom/query.py
index a244b43..9e1e731 100644
--- a/prom/query.py
+++ b/prom/query.py
@@ -2,74 +2,64 @@
 """
 Classes and stuff that handle querying the interface for a passed in Orm class
 """
-from __future__ import unicode_literals, division, print_function, absolute_import
 import copy
-from collections import defaultdict, OrderedDict
-import datetime
-import logging
-import os
-from contextlib import contextmanager
-import math
-import inspect
-import time
+from collections import defaultdict
 import re
 
-from datatypes.collections import ListIterator
-from datatypes import property as cachedproperty
+from datatypes import ListIterator
 
-from .utils import make_list, get_objects, make_dict, make_hash
-from .interface import get_interfaces
 from .compat import *
-
-
-logger = logging.getLogger(__name__)
+from .utils import make_list, get_objects, make_dict
 
 
 class Iterator(ListIterator):
     """The main iterator for all query methods that return iterators
 
-    This is returned from the Query.get() and Query.all() methods, this is also
+    This is returned from the Query.get() method, this is also
     the Iterator class that is set in Orm.iterator_class
 
     fields --
-        ifilter -- callback -- an iterator filter, all yielded rows will be passed
+        ifilter: callback, an iterator filter, all yielded rows will be passed
             through this callback and skipped if ifilter(row) returns False
 
-    examples --
+    :example:
         # iterate through all the primary keys of some orm
-        for pk in SomeOrm.query.all().pk:
+        async for pk in SomeOrm.select_pk().query.get():
             print pk
     """
     @property
     def orm_class(self):
         return self.query.orm_class
 
-    def __init__(self, query):
+    def __init__(self, cursor, query):
         """create an iterator for a query
 
+        :param cursor: object, the cursor object retrieved from the interface
         :param query: Query, the query instance that produced this iterator
         """
-        self.query = query
-
         if query._ifilter:
-            self.ifilter = query._ifilter # https://docs.python.org/2/library/itertools.html#itertools.ifilter
+            # https://docs.python.org/2/library/itertools.html#itertools.ifilter
+            self.ifilter = query._ifilter
 
-        self.reset()
+        self.query = query
 
-    def has_more(self):
-        """Return true if there are more results for this query if the query didn't
-        have a LIMIT clause
+        self._cursor = cursor
+        self._cursor_exhausted = False
+        self.field_names = self.query.fields_select.names()
+
+    async def has_more(self):
+        """Return true if there are more results for this query
 
         :returns: boolean, True if this query could've returned more results
         """
         ret = False
         if self.query.bounds.has_more():
-            cursor = self.cursor()
+            cursor = self._cursor
             # https://www.psycopg.org/docs/cursor.html#cursor.rowcount says that
             # future versions of the spec reserve the right to return None
             if cursor.rowcount == -1 or cursor.rowcount is None:
                 try:
-                    if self[self.query.bounds.find_more_index()]:
+                    if await self[self.query.bounds.find_more_index()]:
                         ret = True
 
                 except IndexError:
@@ -80,50 +70,77 @@ def has_more(self):
 
         return ret
 
-    def cursor(self):
-        cursor = getattr(self, "_cursor", None)
-        if not cursor:
-            cursor = self.query.cursor()
-            self._cursor = cursor
-            self._cursor_i = 0
-            self.field_names = self.query.fields_select.names()
+    async def close(self):
+        """Close the cursor that's tied to this iterator"""
+        await self._cursor.close()
+        self._cursor_exhausted = True
 
-        return cursor
+    async def tolist(self):
+        """Returns this iterator as a list
 
-    def reset(self):
-        """put all the pieces together to build a generator of the results"""
-        self._cursor = None
-        self._cursor_i = 0
+        This is just syntactic sugar around having to do:
+
+            [r async for r in self]
+
+        when a non asyncronous list is needed. It uses the same naming
+        convention as array.tolist which also seems to be used by numpy and
+        pandas also
+
+        https://docs.python.org/3/library/array.html#array.array.tolist
+
+        :returns: list, all the items in the iterator as a list
+        """
+        return [r async for r in self]
 
     def __iter__(self):
-        self.reset()
-        return self
+        """Make sure no one thinks we can iterate through this syncronously"""
+        raise NotImplementedError()
 
-    def next(self):
-        cursor = self.cursor()
-        cursor_next = cursor.__next__
+    async def __aiter__(self):
+        """
+        https://docs.python.org/3/reference/datamodel.html#object.__aiter__
+        """
+        if self._cursor_exhausted:
+            raise ValueError("Cursor has been exhausted, rerun the query")
 
-        # if we have paginated the results we have to account for requesting the
-        # one extra row to see if we have more results waiting
+        cursor_i = 0
+        cursor_limit = -1
         if self.query.bounds.has_more():
-            #if cursor.rownumber == self.query.bounds.limit:
-            if self._cursor_i == self.query.bounds.limit:
-                raise StopIteration()
-
-        o = self.hydrate(cursor_next())
-        self._cursor_i += 1
-        while not self.ifilter(o):
-            o = self.hydrate(cursor_next())
-            self._cursor_i += 1
-        return o
-
-    def count(self):
-        cursor = self.cursor()
+            cursor_limit = self.query.bounds.limit
+
+        try:
+            async for row in self._cursor:
+                if cursor_limit > 0 and cursor_i >= cursor_limit:
+                    break
+
+                o = self.hydrate(row)
+                if self.ifilter(o):
+                    yield o
+
+                cursor_i += 1
+
+        finally:
+            await self.close()
+
+    def __len__(self):
+        """Make sure no one thinks we can get count syncronously"""
+        raise NotImplementedError()
+
+    async def count(self):
+        """Get the number of rows this iterator represents
+
+        Honestly, you probably shouldn't use this and instead do a count query
+        instead since that gives you way more control
+
+        :returns: int, the rows this iterator represents
+        """
+        cursor = self._cursor
         count = cursor.rowcount
 
         if count >= 0:
-            # compensate for having pulled one extra row
-            if self.query.bounds.has_more() and self.query.bounds.limit_paginate == count:
+            bounds = self.query.bounds
+            if bounds.has_more() and bounds.limit_paginate == count:
+                # compensate for having pulled one extra row
                 count -= 1
 
         else:
@@ -133,16 +150,24 @@ def count(self):
             # SQLite cursor's will always have rowcount=-1 until the cursor is
             # exhausted or a fetch*() method has been called, ugh.
             # https://stackoverflow.com/a/839419/5006
-            count = self.query.copy().count()
+            count = await self.query.copy().count()
 
         return count
 
-    def __getitem__(self, i):
-        it = self.copy()
-        q = it.query
+    async def __getitem__(self, i):
+        """Get a specific row or a slice of the rows represented by this
+        iterator
+
+        Honestly, you should probably make specific queries instead of using
+        this since this will make new queries and you won't have control over
+        things like the connection and stuff
 
+        :param i: int|slice, the index(es) you want
+        :returns: Orm|dict|Iterator
+        """
+        q = self.query.copy()
         b = q.bounds
-        limit = b.limit if b.has_limit() else self.count()
+        limit = b.limit if b.has_limit() else await self.count()
         b = q.bounds_class(limit=limit, offset=q.bounds.offset)
 
         if isinstance(i, slice):
@@ -151,53 +176,28 @@ def __getitem__(self, i):
 
             if i.start:
                 start = b.find_offset(i.start)
+
             else:
                 start = b.offset
 
             if i.stop:
                 stop = b.find_offset(i.stop)
+
             else:
                 stop = b.limit
 
             q.limit(stop - start).offset(start)
-            it.query = q
-            return it
+            return type(self)(await q.cursor(), q)
 
         else:
             offset = b.find_offset(i)
 
-            o = q.offset(offset).one()
+            o = await q.offset(offset).one()
             if o is None:
                 raise IndexError("Iterator index {} out of range".format(i))
 
             return o
 
-    def copy(self):
-        q = self.query.copy()
-        return type(self)(q)
-
-    def reverse(self):
-        for f in self.query.fields_sort:
-            f.direction = -f.direction
-        self.reset()
-
-    def __getattr__(self, field_name):
-        """If you have a set of results and just want to grab a certain field then
-        you can do that
-
-        :example:
-            it = FooOrm.query.limit(10).get()
-            it.pk # get all the primary keys from the results
-
-        :param field_name: string, the field name you want the values of
-        :returns: generator, the field_name values
-        """
-        it = self.copy()
-        it.query.fields_select.clear()
-        it.query.select_field(field_name)
-        return it
-        #return (getattr(o, k) for o in self)
-
     def __repr__(self):
         format_str = "[ ... {} ... ]"
         format_args = [self.__class__.__name__]
@@ -212,22 +212,28 @@ def __repr__(self):
     def ifilter(self, o):
         """run o through the filter, if True then orm o should be included
 
-        NOTE -- The ifilter callback needs to account for non Orm instance values of o
+        NOTE -- The ifilter callback needs to account for non Orm instance
+            values of o
 
-        :param o: Orm|mixed, usually an Orm instance but can also be a tuple or single value
+        :param o: Orm|tuple[Any]|Any, usually an Orm instance but can also be a
+            tuple or single value
         :returns: boolean, True if o should be filtered
         """
-        return o
+        return True
 
     def hydrate(self, d):
-        """Prepare the raw dict d returned from the interface cursor to be returned
-        by higher level objects, this will usually mean hydrating an Orm instance
-        or stuff like that
+        """Prepare the raw dict d returned from the interface cursor to be
+        returned by higher level objects, this will usually mean hydrating an
+        Orm instance or stuff like that
+
+        NOTE -- this will call Orm.hydrate and should be the only place that
+            will call that method
 
         :param d: dict, the raw dict cursor result returned from the interface
-        :returns: mixed, usually an Orm instance populated with d but can also be
-            a tuple if the query selected more than one field. If the query selected
-            one field then just that value will be returned
+        :returns: Orm|tuple[Any]|Any, usually an Orm instance populated with d
+            but can also be a tuple if the query selected more than one field.
+            If the query selected one field then just that value will be
+            returned
         """
         r = None
         orm_class = self.orm_class
@@ -243,6 +249,7 @@ def hydrate(self, d):
         else:
             if orm_class:
                 r = orm_class.hydrate(d)
+
             else:
                 r = d
 
@@ -250,6 +257,9 @@ def hydrate(self, d):
 
 
 class QueryBounds(object):
+    """Stores the bounds (eg, DESC, ASC) information for a Query. It has a lot
+    of hooks to make setting limit and offset easier
+    """
     @property
     def limit(self):
         l = self._limit
@@ -348,8 +358,9 @@ def has_limit(self):
         return self.limit > 0
 
     def has_more(self):
-        """Returns True if the current bounds are set up to query one extra row in order
-        to make pagination easier (to know if there should be a next link)
+        """Returns True if the current bounds are set up to query one extra row
+        in order to make pagination easier (to know if there should be a next
+        link)
 
         :returns: boolean
         """
@@ -365,8 +376,8 @@ def __str__(self):
         return "limit: {}, offset: {}".format(self.limit, self.offset)
 
     def find_offset(self, i):
-        """Given an index i, use the current offset and limit to find the correct
-        offset i would be
+        """Given an index i, use the current offset and limit to find the
+        correct offset i would be
 
         :param i: int, the index used to find the new offset
         :returns: int, the new offset
@@ -393,6 +404,7 @@ def find_more_index(self):
 
 
 class QueryField(object):
+    """Holds information for a field in a WHERE clause"""
     @property
     def schema(self):
         return self.query.schema if self.query else None
@@ -463,6 +475,7 @@ def parse(self, field_name, schema):
 
 
 class QueryFields(list):
+    """Holds all the QueryField instances for a given clause"""
     @property
     def fields(self):
         """Returns a dict of field_name: field_value"""
@@ -505,11 +518,17 @@ def clear(self):
 class Query(object):
     """Handle standard query creation and allow interface querying
 
+    This is the glue between Orm and Interface. For the most part you will never
+    interact with the Interface directly but instead use Orm and Query, and you
+    won't usually ever instantiate this class directly but instead use the 
+    Orm.query magic class property
+
     :example:
         q = Query(orm_class)
-        q.eq_foo(1).desc_bar().limit(10).page(2).get()
+        q.select_bar().select_che().eq_foo(1).desc_bar().limit(10).page(2).get()
         # SELECT
-        #   *
+        #   bar,
+        #   che
         # FROM
         #   <orm_class.table_name>
         # WHERE
@@ -536,8 +555,8 @@ def schema(self):
 
     @property
     def schemas(self):
-        """Find and return all the schemas that are needed for this query to complete
-        successfully
+        """Find and return all the schemas that are needed for this query to
+        complete successfully
 
         Another way to put this is all the schemas this query touches
 
@@ -558,14 +577,15 @@ def schemas(self):
 
     @property
     def OR(self):
-        """Wraps left and right field statements with an OR clause, you can chain
-        as many OR calls as you want to create an any length OR clause
+        """Wraps left and right field statements with an OR clause, you can
+        chain as many OR calls as you want to create an any length OR clause
 
-        I don't love that I had to use OR instead of or but "or" is a reserved
+        I don't love that I had to use OR instead of "or" but "or" is a reserved
         keyword and I thought OR was better than like _or, or_ or _or_
 
         :Example:
-            self.eq_foo(1).OR.eq_foo(5).OR.eq_foo(10) # (foo=1 OR foo=5 OR foo=10)
+            self.eq_foo(1).OR.eq_foo(5).OR.eq_foo(10)
+            # (foo=1 OR foo=5 OR foo=10)
         """
         self.fields_where[-1].or_clause = True
         return self
@@ -574,6 +594,8 @@ def OR(self):
     def or_(self): return self.OR
     @property
     def _or(self): return self.OR
+    @property
+    def _or_(self): return self.OR
 
     @property
     def AND(self):
@@ -586,10 +608,12 @@ def AND(self):
     def and_(self): return self.AND
     @property
     def _and(self): return self.AND
+    @property
+    def _and_(self): return self.AND
 
     def __init__(self, orm_class=None, **kwargs):
-        # needed to use the db querying methods like get(), if you just want to build
-        # a query then you don't need to bother passing this in
+        # needed to use the db querying methods like get(), if you just want to
+        # build a query then you don't need to bother passing this in
         self.orm_class = orm_class
         self.reset()
 
@@ -606,40 +630,39 @@ def ref(self, orm_classpath):
         """
         takes a classpath to allow query-ing from another Orm class
 
-        the reason why it takes string paths is to avoid infinite recursion import 
-        problems because an orm class from module A might have a ref from module B
-        and sometimes it is handy to have module B be able to get the objects from
-        module A that correspond to the object in module B, but you can't import
-        module A into module B because module B already imports module A.
+        the reason why it takes string paths is to avoid infinite recursion
+        import problems because an orm class from module A might have a ref
+        from module B and sometimes it is handy to have module B be able to get
+        the objects from module A that correspond to the object in module B,
+        but you can't import module A into module B because module B already
+        imports module A.
 
-        :param orm_classpath: string|type, a full python class path (eg, foo.bar.Che) or
-            an actual model.Orm python class
-        return -- Query()
+        :param orm_classpath: string|type, a full python class path (eg,
+            foo.bar.Che) or an actual model.Orm python class
+        return -- Query
         """
         # split orm from module path
         if isinstance(orm_classpath, basestring):
             orm_module, orm_class = get_objects(orm_classpath)
+
         else:
             orm_class = orm_classpath
 
         return orm_class.query
 
     def ref_class(self, orm_classpath):
+        """alias to .ref, here to match Orm.ref_class syntax"""
         return self.ref(orm_classpath)
 
-    def __iter__(self):
-        return self.get()
-
     def find_methods(self, method_name):
-        """Given a method name like <OPERATOR>_<FIELD_NAME> or <FIELD_NAME>_<OPERATOR>,
-        split those into <OPERATOR> and <FIELD_NAME> if there is an existing
-        <OPERATOR>_field method that exists
+        """Given a method name like <OPERATOR>_<FIELD_NAME> or 
+        <FIELD_NAME>_<OPERATOR>, split those into <OPERATOR> and <FIELD_NAME>
+        if there is an existing <OPERATOR>_field method that exists
 
         :example:
             self.gt_foo() # (<gt_field>, None, "foo")
-            self.one_pk(value) # (<eq_field>, <one>, "pk")
 
-        :returns: tuple, (<FIELD_METHOD>, <QUERY_METHOD>, <FIELD_NAME>)
+        :returns: tuple[str, str], (<FIELD_METHOD>, <FIELD_NAME>)
         """
         # infinite recursion check, if a *_field method gets in here then it
         # doesn't exist
@@ -651,28 +674,19 @@ def find_methods(self, method_name):
             name, field_name = method_name.split("_", 1)
 
         except ValueError:
-            raise AttributeError("invalid potential method: {}".format(method_name))
+            raise AttributeError("invalid potential method: {}".format(
+                method_name
+            ))
 
         else:
-
             if not name:
-                raise AttributeError('Could not resolve methods from {}"'.format(
-                    method_name
-                ))
-
-            elif name in {"one", "value"}:
-                field_method = self.eq_field
-                query_method = getattr(self, name)
-
-            elif name in {"get", "values", "all", "cursor"}:
-                field_method = self.in_field
-                query_method = getattr(self, name)
-
-            elif name in {"raw"}:
-                raise AttributeError(method_name)
+                raise AttributeError(
+                    'Could not resolve methods from {}"'.format(
+                        method_name
+                    )
+                )
 
             else:
-                query_method = None
                 field_method_name = "{}_field".format(name)
                 field_method = getattr(self, field_method_name, None)
                 if not field_method:
@@ -682,36 +696,47 @@ def find_methods(self, method_name):
                     field_method = getattr(self, field_method_name, None)
 
                 if not field_method:
-                    raise AttributeError('No field method derived from {}'.format(
-                        method_name
-                    ))
+                    raise AttributeError(
+                        'No field method derived from {}'.format(
+                            method_name
+                            )
+                        )
 
-        return field_method, query_method, field_name
+        return field_method, field_name
 
     def __getattr__(self, method_name):
-        field_method, query_method, field_name = self.find_methods(method_name)
+        """Allows fluid interface of .<OPERATOR>_<FIELD_NAME>(<FIELD_VALUE>)
+
+        :returns: callback, this will return a callback that wraps a 
+            *_field method and passes in <FIELD_NAME> to the *_field method
+        """
+        field_method, field_name = self.find_methods(method_name)
 
         def callback(*args, **kwargs):
-            #pout.v(args, kwargs, field_method, query_method, field_name)
             if field_method:
                 ret = field_method(field_name, *args, **kwargs)
 
-            if query_method:
-                ret = query_method()
-
             return ret
 
         return callback
 
     def create_field(self, field_name, field_val=None, **kwargs):
+        """Creates a QueryField instance"""
         f = self.field_class(self, field_name, field_val, **kwargs)
         return f
 
-    def create_iterator(self, query):
+    def create_iterator(self, cursor):
+        """Creates a query.Iterator instance wrapping cursor
+
+        :param cursor: object, this is retrieved from the Interface
+        :returns: Iterator
+        """
         if self.orm_class:
-            iterator = self.orm_class.iterator_class(query)
+            iterator = self.orm_class.iterator_class(cursor, query=self)
+
         else:
-            iterator = self.iterator_class(query)
+            iterator = self.iterator_class(cursor, query=self)
+
         return iterator
 
     def append_compound(self, operator, queries, **kwargs):
@@ -730,14 +755,16 @@ def append_compound(self, operator, queries, **kwargs):
         return self
 
     def append_operation(self, operator, field_name, field_val=None, **kwargs):
+        """Internal method that appends an operation to the where clause"""
         kwargs["operator"] = operator
         f = self.create_field(field_name, field_val, **kwargs)
         self.fields_where.append(f)
         return self
 
     def append_sort(self, direction, field_name, field_val=None, **kwargs):
-        """
-        sort this query by field_name in directrion
+        """Internal method that appends a sort to the sort clause
+
+        sort this query by field_name in direction
 
         used to be named sort_field
 
@@ -747,8 +774,10 @@ def append_sort(self, direction, field_name, field_val=None, **kwargs):
         """
         if direction > 0:
             direction = 1
+
         elif direction < 0:
             direction = -1
+
         else:
             raise ValueError("direction {} is undefined".format(direction))
 
@@ -759,11 +788,13 @@ def append_sort(self, direction, field_name, field_val=None, **kwargs):
         return self
 
     def distinct(self, *field_names):
+        """Mark the passed in query select fields as distinct"""
         self.fields_select.options["distinct"] = True
         return self.select(*field_names)
 
     def select_field(self, field_name):
-        """set a field to be selected, this is automatically called when you do select_FIELDNAME(...)"""
+        """set a field to be selected, this is automatically called when you do
+        select_FIELDNAME(...)"""
         if field_name == "*":
             self.fields_select.options["all"] = True
         else:
@@ -772,7 +803,8 @@ def select_field(self, field_name):
         return self
 
     def select(self, *field_names):
-        """set multiple fields to be selected"""
+        """set multiple fields to be selected, this is the many version of 
+        .select_field"""
         for field_name in make_list(field_names):
             self.select_field(field_name)
         return self
@@ -781,15 +813,16 @@ def set_field(self, field_name, field_val):
         """
         set a field into .fields_set attribute
 
-        In insert/update queries, these are the fields that will be inserted/updated into the db
+        In insert/update queries, these are the fields that will be
+        inserted/updated into the db
         """
         field = self.create_field(field_name, field_val)
         self.fields_set.append(field)
         return self
 
     def set(self, fields=None, **fields_kwargs):
-        """
-        completely replaces the current .fields with fields and fields_kwargs combined
+        """completely replaces the current .fields with fields and fields_kwargs
+        combined, this is the many version of .set_field
         """
         fields = make_dict(fields, fields_kwargs)
         for field_name, field_val in fields.items():
@@ -829,62 +862,122 @@ def difference(self, *queries, **kwargs):
         Return the rows with rows in the first query that are not in the other
         queries
 
-        This is named .difference() instead of .except() because except is a python
-        keyword.
+        This is named .difference() instead of .except() because except is a
+        python keyword.
 
         See .intersect() since this works very similar
         """
         return self.append_compound("except", queries, **kwargs)
 
-    def is_field(self, field_name, field_val=None, **field_kwargs):
-        return self.eq_field(field_name, field_val, **field_kwargs)
     def eq_field(self, field_name, field_val=None, **field_kwargs):
-        return self.append_operation("eq", field_name, field_val, **field_kwargs)
+        """<FIELD_NAME> = <FIELD_VALUE>"""
+        return self.append_operation(
+            "eq",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
-    def not_field(self, field_name, field_val=None, **field_kwargs):
-        return self.ne_field(field_name, field_val, **field_kwargs)
     def ne_field(self, field_name, field_val=None, **field_kwargs):
-        return self.append_operation("ne", field_name, field_val, **field_kwargs)
+        """<FIELD_NAME> != <FIELD_VALUE>"""
+        return self.append_operation(
+            "ne",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def between_field(self, field_name, low, high):
+        """<FIELD_NAME> >= low AND <FIELD_NAME> <= high"""
         self.gte_field(field_name, low)
         self.lte_field(field_name, high)
         return self
 
     def lte_field(self, field_name, field_val=None, **field_kwargs):
-        return self.append_operation("lte", field_name, field_val, **field_kwargs)
+        """<FIELD_NAME> <= <FIELD_VALUE>"""
+        return self.append_operation(
+            "lte",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def lt_field(self, field_name, field_val=None, **field_kwargs):
-        return self.append_operation("lt", field_name, field_val, **field_kwargs)
+        """<FIELD_NAME> < <FIELD_VALUE>"""
+        return self.append_operation(
+            "lt",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def gte_field(self, field_name, field_val=None, **field_kwargs):
-        return self.append_operation("gte", field_name, field_val, **field_kwargs)
+        """<FIELD_NAME> >= <FIELD_VALUE>"""
+        return self.append_operation(
+            "gte",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def gt_field(self, field_name, field_val=None, **field_kwargs):
-        return self.append_operation("gt", field_name, field_val, **field_kwargs)
+        """<FIELD_NAME> > <FIELD_VALUE>"""
+        return self.append_operation(
+            "gt",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def in_field(self, field_name, field_val=None, **field_kwargs):
-        """
+        """<FIELD_NAME> IN (<FIELD_VALUE>)
+
         :param field_val: list, a list of field_val values
         """
         field_kwargs["is_list"] = True
-        return self.append_operation("in", field_name, field_val, **field_kwargs)
+        return self.append_operation(
+            "in",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def nin_field(self, field_name, field_val=None, **field_kwargs):
-        """
+        """<FIELD_NAME> NOT IN (<FIELD_VALUE>)
+
         :param field_val: list, a list of field_val values
         """
         field_kwargs["is_list"] = True
-        return self.append_operation("nin", field_name, field_val, **field_kwargs)
+        return self.append_operation(
+            "nin",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def startswith_field(self, field_name, field_val, **field_kwargs):
-        return self.like_field(field_name, "{}%".format(field_val), **field_kwargs)
+        """<FIELD_NAME> LIKE <FIELD_VALUE>%"""
+        return self.like_field(
+            field_name,
+            "{}%".format(field_val),
+            **field_kwargs
+        )
 
     def endswith_field(self, field_name, field_val, **field_kwargs):
-        return self.like_field(field_name, "%{}".format(field_val), **field_kwargs)
+        """<FIELD_NAME> LIKE %<FIELD_VALUE>"""
+        return self.like_field(
+            field_name,
+            "%{}".format(field_val),
+            **field_kwargs
+        )
 
     def contains_field(self, field_name, field_val, **field_kwargs):
-        return self.like_field(field_name, "%{}%".format(field_val), **field_kwargs)
+        """<FIELD_NAME> LIKE %<FIELD_VALUE>%"""
+        return self.like_field(
+            field_name,
+            "%{}%".format(field_val),
+            **field_kwargs
+        )
 
     def like_field(self, field_name, field_val, **field_kwargs):
         """Perform a field_name LIKE field_val query
@@ -895,7 +988,13 @@ def like_field(self, field_name, field_val, **field_kwargs):
         """
         if not field_val:
             raise ValueError("Cannot LIKE nothing")
-        return self.append_operation("like", field_name, field_val, **field_kwargs)
+
+        return self.append_operation(
+            "like",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def nlike_field(self, field_name, field_val, **field_kwargs):
         """Perform a field_name NOT LIKE field_val query
@@ -906,30 +1005,46 @@ def nlike_field(self, field_name, field_val, **field_kwargs):
         """
         if not field_val:
             raise ValueError("Cannot NOT LIKE nothing")
-        return self.append_operation("nlike", field_name, field_val, **field_kwargs)
+
+        return self.append_operation(
+            "nlike",
+            field_name,
+            field_val,
+            **field_kwargs
+        )
 
     def raw_field(self, field_name, *field_vals, **field_kwargs):
         field_kwargs["raw"] = True
-        return self.append_operation("raw", field_name, field_vals, **field_kwargs)
+        return self.append_operation(
+            "raw",
+            field_name,
+            field_vals,
+            **field_kwargs
+        )
 
     def asc(self, *field_names):
+        """the many version of .asc_field"""
         for field_name in field_names:
             self.asc_field(field_name)
         return self
 
     def asc_field(self, field_name, field_val=None):
+        """<FIELD_NAME> ASC"""
         return self.append_sort(1, field_name, field_val)
 
     def desc(self, *field_names):
+        """the many version of .desc_field"""
         for field_name in field_names:
             self.desc_field(field_name)
         return self
 
     def desc_field(self, field_name, field_val=None):
+        """<FIELD_NAME> DESC"""
         return self.append_sort(-1, field_name, field_val)
 
     def ifilter(self, predicate):
-        """Set the predicate (callback) for an iterator returned from this instance
+        """Set the predicate (callback) for an iterator returned from this
+        instance
 
         https://docs.python.org/2/library/itertools.html#itertools.ifilter
 
@@ -940,62 +1055,122 @@ def ifilter(self, predicate):
         self._ifilter = predicate
         return self
 
-    def filter(self, predicate):
-        return self.ifilter(predicate)
-
     def limit(self, limit):
+        """LIMIT <limit>"""
         self.bounds.limit = limit
         return self
 
     def offset(self, offset):
+        """OFFSET <offset>"""
         self.bounds.offset = offset
         return self
 
     def page(self, page):
+        """OFFSET <PAGE_CONVERTED_TO_OFFSET>"""
         self.bounds.page = page
         return self
 
-    def cursor(self):
-        """Used by the Iterator to actually query the db"""
-        return self.execute('get', cursor_result=True)
+    def copy(self):
+        """nice handy wrapper around the deepcopy"""
+        return copy.deepcopy(self)
 
-    def get(self):
+    def __deepcopy__(self, memodict={}):
+        instance = type(self)(self.orm_class)
+        ignore_keys = set(["_interface", "interface"])
+        for key, val in self.__dict__.items():
+            if key not in ignore_keys:
+                setattr(instance, key, copy.deepcopy(val, memodict))
+        return instance
+
+    def __str__(self):
+        """Attempt to render the query, this should be used mainly for debugging
+        and not in logs or things like that since this can be intense and
+        multiline and also wrong
+        """
+        try:
+            return self.render()
+
+        except AttributeError:
+            return super().__str__()
+
+    def render(self, **kwargs):
+        """Render the query
+
+        :returns: string, the rendered query, this is not assured to be a valid
+            query but is handy for quickly debugging what the query roughly
+            looks like
+        """
+        return self.interface.render(self.schema, self, **kwargs)
+
+    async def __aiter__(self):
+        """Allows fluid interface for iterating through the query
+
+        :Example:
+            [r async for r in q]
+        """
+        async for row in await self.get():
+            yield row
+
+    async def tolist(self, **kwargs):
+        """Wraps the Iterator.tolist method and does the same thing"""
+        iterator = await self.get(**kwargs)
+        return await iterator.tolist()
+
+    async def raw(self, query_str, *query_args, **kwargs):
+        """Send a raw query to the interface without any processing
+
+        NOTE -- This will allow you to make any raw query and will usually
+        return raw results
+
+        :param query_str: str, the raw query for whatever the backend interface
+            is
+        :param query_args: list, if you have named parameters, you can pass in
+            the values
+        :param **kwargs: passed directly to the interface
+        :returns: Any, depends on the interface and the type of query
+        """
+        return await self.interface.raw(query_str, *query_args, **kwargs)
+
+    async def cursor(self, **kwargs):
+        """Return a raw Interface cursor
+
+        :param **kwargs: passed directly to the interface
+        :returns: object, a cursor instance that can retrieve results from the
+            interface's backend
+        """
+        return await self.interface.get(
+            self.schema,
+            self,
+            cursor_result=True,
+            **kwargs
+        )
+
+    async def get(self, **kwargs):
         """
         get results from the db
 
         :returns: Iterator
         """
-        self.bounds.paginate = True
-        return self.create_iterator(self)
+        self.bounds.paginate = kwargs.pop("paginate", True)
+        cursor = await self.cursor(**kwargs)
+        return self.create_iterator(cursor)
 
-    def all(self):
-        return self.get()
+    async def one(self, **kwargs):
+        """get one row from the db"""
+        ret = None
 
-    def values(self):
-        if not self.fields_select:
-            raise ValueError("No selected fields")
-        return self.get()
+        kwargs["paginate"] = False
+        async for ret in await self.limit(1).get(**kwargs):
+            break
 
-    def one(self):
-        """get one row from the db"""
-        self.limit(1)
-        self.bounds.paginate = False
-        it = self.create_iterator(self)
-        try:
-            ret = it.next()
-        except StopIteration:
-            ret = None
         return ret
 
-    def value(self):
-        """convenience method to just get one value or tuple of values for the query"""
-        if not self.fields_select:
-            raise ValueError("no selected fields")
-        return self.one()
-
-    def count(self):
-        """return the count of the criteria"""
+    async def count(self, **kwargs):
+        """return the row count of the criteria
 
+        :param **kwargs: passed through to the interface
+        :returns: int, the row count
+        """
         # sorting shouldn't matter for a count query
         fields_sort = self.fields_sort
         self.fields_sort = self.fields_sort_class()
@@ -1008,7 +1183,7 @@ def count(self):
         bounds = self.bounds
         self.bounds = self.bounds_class()
 
-        ret = self.execute('count')
+        ret = await self.interface.count(self.schema, self, **kwargs)
 
         # restore previous values now that count is done
         self.fields_sort = fields_sort
@@ -1026,29 +1201,49 @@ def count(self):
 
         return ret
 
-    def has(self):
-        """returns true if there is atleast one row in the db matching the query, False otherwise"""
-        v = self.one()
+    async def has(self, **kwargs):
+        """returns true if there is atleast one row in the db matching the
+        query, False otherwise
+
+        :returns: bool
+        """
+        v = await self.one(**kwargs)
         return True if v else False
 
-    def exists(self):
-        return self.has()
+    async def insert(self, **kwargs):
+        """persist the .fields that were set with .set_field and .set
 
-    def insert(self, **kwargs):
-        """persist the .fields"""
-        return self.interface.insert(self.schema, self.fields_set.fields, **kwargs)
+        :returns: int|str|None, the primary key of the inserted row if it exists
+        """
+        return await self.interface.insert(
+            self.schema,
+            self.fields_set.fields,
+            **kwargs
+        )
 
-    def update(self, **kwargs):
-        """persist the .fields using .fields_where"""
-        return self.interface.update(
+    async def update(self, **kwargs):
+        """persist the .fields set in .set and .set_field using .fields_where"""
+        return await self.interface.update(
             self.schema,
             self.fields_set.fields,
             self,
             **kwargs
         )
 
-    def upsert(self, conflict_field_names=None, **kwargs):
-        """persist the .fields"""
+    async def upsert(self, conflict_field_names=None, **kwargs):
+        """persist the .fields set with .set and .set_field
+
+        The insert fields are all the fields set in .fields.
+
+        The update fields are all the fields set in .fields minus the conflict
+        fields
+
+        :param conflict_field_names: list, the list of field names that will be
+            checked for conflicts and will insert if these field values don't
+            already exist in the db or update if they do
+        :param **kwargs: passed through to the interface upsert method
+        :returns: str|int|None, the primary key of the inserted/updated row
+        """
         insert_fields = self.fields_set.fields
         update_fields = dict(insert_fields)
 
@@ -1058,7 +1253,7 @@ def upsert(self, conflict_field_names=None, **kwargs):
         for field_name in conflict_field_names:
             update_fields.pop(field_name, None)
 
-        return self.interface.upsert(
+        return await self.interface.upsert(
             self.schema,
             insert_fields,
             update_fields,
@@ -1066,56 +1261,11 @@ def upsert(self, conflict_field_names=None, **kwargs):
             **kwargs
         )
 
-    def delete(self, **kwargs):
+    async def delete(self, **kwargs):
         """remove fields matching the where criteria"""
-        return self.execute('delete', **kwargs)
-
-    def render(self, **kwargs):
-        """Render the query
-
-        :returns: string, the rendered query, this is not assured to be a valid query
-            but is handy for quickly debugging what the query roughly looks like
-        """
-        return self.interface.render(self.schema, self, **kwargs)
-
-    def raw(self, query_str, *query_args, **query_options):
-        """
-        use the interface.query() method to pass in your own raw query without
-        any processing
-
-        NOTE -- This will allow you to make any raw query and will usually return
-        raw results, it won't wrap those results in a prom.Orm iterator instance
-        like other methods like .all() and .get()
-
-        query_str -- string -- the raw query for whatever the backend interface is
-        query_args -- list -- if you have named parameters, you can pass in the values
-        **query_options -- dict -- key:val options for the backend, these are very backend specific
-        return -- mixed -- depends on the backend and the type of query
-        """
-        i = self.interface
-        return i.raw(query_str, *query_args, **query_options)
-
-    def execute(self, method_name, **kwargs):
-        i = self.interface
-        s = self.schema
-        return getattr(i, method_name)(s, self, **kwargs)
-
-    def copy(self):
-        """nice handy wrapper around the deepcopy"""
-        return copy.deepcopy(self)
-
-    def __deepcopy__(self, memodict={}):
-        instance = type(self)(self.orm_class)
-        ignore_keys = set(["_interface", "interface"])
-        for key, val in self.__dict__.items():
-            if key not in ignore_keys:
-                setattr(instance, key, copy.deepcopy(val, memodict))
-        return instance
-
-    def __str__(self):
-        try:
-            return self.render()
-
-        except AttributeError:
-            return super().__str__()
+        return await self.interface.delete(
+            self.schema,
+            self,
+            **kwargs
+        )
 
diff --git a/prom/utils.py b/prom/utils.py
index 0e6009d..d9b40be 100644
--- a/prom/utils.py
+++ b/prom/utils.py
@@ -1,48 +1,9 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import importlib
-import heapq
-import itertools
-import os
-import sys
-import codecs
-from contextlib import contextmanager
 
 from .compat import *
 
 
-class Stream(object):
-    """In the CLI we either want to print to stdout or write to a file, this wrapper
-    does that, if path is empty then anything you write to it will write to stdout
-    """
-    def __init__(self, path=""):
-        self.path = path
-        if path:
-            self.path = os.path.abspath(os.path.expanduser(str(path)))
-            self.stream = codecs.open(self.path, encoding='utf-8', mode='w+')
-        else:
-            self.stream = sys.stdout
-
-    def __getattr__(self, k):
-        return getattr(self.stream, k)
-
-    @contextmanager
-    def open(self):
-        yield self
-        if self.path:
-            self.stream.close()
-
-    def write_line(self, line, count=1):
-        """writes the line and count newlines after the line"""
-        self.write(line)
-        self.write_newlines(count)
-
-    def write_newlines(self, count=1):
-        """writes count newlines"""
-        for c in range(count):
-            self.write("\n")
-
-
 def get_objects(classpath, calling_classpath=""):
     """
     given a classpath like foo.bar.Baz return module foo.bar and class Baz
@@ -52,36 +13,24 @@ def get_objects(classpath, calling_classpath=""):
         https://docs.python.org/2.5/whatsnew/pep-328.html
         https://www.python.org/dev/peps/pep-0328/
 
-    :param classpath: string, the full python class path (includes modules), a classpath
-        is something like foo.bar.Che where Che is the class definied in the foo.bar
-        module
-    :param calling_classpath: string, if classpath is relative (eg, ..foo.Bar) then
-        this is needed to resolve the relative classpath, it is usually the path of
-        the module that is calling get_objects()
+    :param classpath: string, the full python class path (includes modules), a
+        classpath is something like foo.bar.Che where Che is the class definied
+        in the foo.bar module
+    :param calling_classpath: string, if classpath is relative (eg, ..foo.Bar)
+        then this is needed to resolve the relative classpath, it is usually the
+        path of the module that is calling get_objects()
     :returns: tuple, (module, class)
     """
-#     if classpath.startswith("."):
-#         rel_count = len(re.match("^\.+", classpath).group(0))
-#         if calling_classpath:
-#             calling_count = calling_classpath.count(".")
-#             if rel_count > calling_count:
-#                 raise ValueError(
-#                     "Attempting relative import passed calling_classpath {}".format(
-#                         calling_classpath
-#                     )
-#                 )
-# 
-#             bits = calling_classpath.rsplit('.', rel_count)
-#             parent_classpath = bits[0]
-#             classpath = ".".join([parent_classpath, classpath[rel_count:]])
-# 
-#         else:
-#             raise ValueError("Attempting relative import without calling_classpath")
-# 
-    module_name, class_name = classpath.rsplit('.', 1)
+    if ":" in classpath:
+        module_name, class_name = classpath.rsplit(":", 1)
+
+    else:
+        module_name, class_name = classpath.rsplit(".", 1)
+
     module = importlib.import_module(module_name, calling_classpath)
     try:
         klass = getattr(module, class_name)
+
     except AttributeError:
         raise AttributeError("module {} has no attribute {} parsing {}".format(
             module.__name__,
@@ -95,7 +44,8 @@ def get_objects(classpath, calling_classpath=""):
 def make_list(vals):
     """make vals a list, no matter what
 
-    :param vals: list, vals should always be a list or tuple (eg, *args passed as args)
+    :param vals: list, vals should always be a list or tuple (eg, *args passed
+        as args)
     """
     ret = []
     if isinstance(vals, basestring):
@@ -146,10 +96,3 @@ def make_dict(fields, fields_kwargs):
     return ret
 
 
-def make_hash(*mixed):
-    s = ""
-    for m in mixed:
-        s += String(m)
-    # http://stackoverflow.com/questions/5297448/how-to-get-md5-sum-of-a-string
-    return String(s).md5()
-
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..98b162e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,74 @@
+[project]
+requires-python = ">=3.10"
+description = "A sensible orm for PostgreSQL or SQLite"
+authors = [
+  { name = "Jay Marcyes", email = "jay@marcyes.com" }
+]
+keywords = [
+  "async",
+  "postgresql",
+  "postgres",
+  "database",
+  "db",
+  "sqlite",
+  "orm",
+]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Environment :: Web Environment",
+  "Environment :: Other Environment",
+  "Environment :: Console",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: MIT License",
+  "Operating System :: OS Independent",
+  "Topic :: Database",
+  "Topic :: Software Development :: Libraries",
+  "Topic :: Utilities",
+  "Programming Language :: Python :: 3"
+]
+dependencies = [
+  "dsnparse",
+  "datatypes",
+]
+name = "prom"
+dynamic = [
+  "version"
+]
+readme = "README.md"
+license = { file = "LICENSE.txt" }
+
+[project.urls]
+Homepage = "https://github.com/Jaymon/prom"
+Repository = "https://github.com/Jaymon/prom"
+
+[project.optional-dependencies]
+tests = [
+  "testdata"
+]
+sqlite = [
+  "aiosqlite"
+]
+postgres = [
+  "psycopg" # psycopg[binary]
+]
+
+[build-system]
+requires = [
+  "setuptools>=62.3.0"
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+exclude = [
+  "tests*",
+  "example*",
+  "*_test*",
+  "docs*"
+]
+include = [
+  "prom*"
+]
+
+[tool.setuptools.dynamic]
+version = { attr = "prom.__version__" }
+
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 9fb1905..0000000
--- a/setup.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python
-# http://docs.python.org/distutils/setupscript.html
-# http://docs.python.org/2/distutils/examples.html
-# https://packaging.python.org/
-# https://packaging.python.org/tutorials/distributing-packages/
-from setuptools import setup, find_packages
-import re
-import os
-from codecs import open
-
-
-name = "prom"
-kwargs = dict(
-    name=name,
-    description='A sensible orm for PostgreSQL or SQLite',
-    author='Jay Marcyes',
-    author_email='jay@marcyes.com',
-    url='http://github.com/jaymon/{}'.format(name),
-    license="MIT",
-    classifiers=[ # https://pypi.python.org/pypi?:action=list_classifiers
-        'Development Status :: 4 - Beta',
-        'Environment :: Web Environment',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Topic :: Database',
-        'Topic :: Software Development :: Libraries',
-        'Topic :: Utilities',
-        'Programming Language :: Python :: 3.10',
-    ],
-#     entry_points = {
-#         'console_scripts': [
-#             '{} = {}.__main__:console'.format(name, name),
-#         ],
-#     }
-
-)
-
-kwargs["tests_require"] = ['testdata']
-kwargs["install_requires"] = ['dsnparse', 'datatypes']
-kwargs["extras_require"] = {
-    'postgres': ["psycopg", "psycogreen", "gevent"],
-}
-
-
-def read(path):
-    if os.path.isfile(path):
-        with open(path, encoding='utf-8') as f:
-            return f.read()
-    return ""
-
-
-vpath = os.path.join(name, "__init__.py")
-if os.path.isfile(vpath):
-    kwargs["packages"] = find_packages(exclude=["tests", "tests.*", "*_test*", "example*"])
-
-    dpath = os.path.join(name, "data")
-    if os.path.isdir(dpath):
-        # https://docs.python.org/3/distutils/setupscript.html#installing-package-data
-        kwargs["package_data"] = {name: ['data/*']} 
-
-else:
-    vpath = "{}.py".format(name)
-    kwargs["py_modules"] = [name]
-
-kwargs["version"] = re.search(r"^__version__\s*=\s*[\'\"]([^\'\"]+)", read(vpath), flags=re.I | re.M).group(1)
-
-# https://pypi.org/help/#description-content-type
-kwargs["long_description"] = read('README.md')
-kwargs["long_description_content_type"] = "text/markdown"
-
-
-setup(**kwargs)
-


Test patch:
diff --git a/prom/extras/testdata.py b/prom/extras/testdata.py
index d140876..f0ff1cb 100644
--- a/prom/extras/testdata.py
+++ b/prom/extras/testdata.py
@@ -19,15 +19,15 @@
 
 
 class ModelData(TestData):
-    """Provides testdata hooks for the projects models, testdata is a python module
-    for generating random data for testing
+    """Provides testdata hooks for the projects models, testdata is a python
+    module for generating random data for testing
 
     https://github.com/Jaymon/testdata
 
-    This uses a lot of magic and I'm not sure the best way to describe it, basically,
-    this will create get_*, create_*, and get_*_fields methods for any Orm subclass
-    you have loaded into memory. The * will correspond to Orm.model_name and 
-    Orm.models_name.
+    This uses a lot of magic and I'm not sure the best way to describe it,
+    basically, this will create get_*, create_*, and get_*_fields methods for
+    any Orm subclass you have loaded into memory. The * will correspond to
+    Orm.model_name and Orm.models_name.
 
     Because it only creates those methods for Orm classes loaded
     into memory you need to import the modules for the classes you want to test
@@ -43,40 +43,43 @@ class Foobar(Orm):
         import testdata
         from prom.extras.testdata import ModelData
 
-        o = testdata.get_foobar()
-        print(o.pk) # None
+        o = await testdata.get_foobar()
+        print(o.pk) # None because .save was not called
 
-        o = testdata.create_foobar()
+        o = await testdata.create_foobar()
         print(o.pk) # pk will be set because .save() was called
 
-        fields = testdata.get_foobar_fields()
+        fields = await testdata.get_foobar_fields()
         print(fields) # dict that can be used create a Foobar(fields) instance
 
-        os = testdata.get_foobars(foobar_count=2) # will return a list of 2 Foobar instances
+        # will return a list of 2 Foobar instances
+        os = await testdata.get_foobars(foobar_count=2)
 
-        os = testdata.create_foobars(foobar_count=2) # will return a list of 2 created Foobar instances
+        # will return a list of 2 created Foobar instances
+        os = await testdata.create_foobars(foobar_count=2)
 
 
     To customize a method, you can extend this class and define the method. The
     signature for any overridden methods is (self, orm_class, **kwargs),
-    and you will usually just want to override the get_<MODELS-NAME>_fields class
-    since that is the one the get_* and create_* methods use to populate the orms
+    and you will usually just want to override the get_<MODELS-NAME>_fields
+    class since that is the one the get_* and create_* methods use to populate
+    the orms
 
     All the signatures of all the methods need to be the same because they are
     mixed and matched, so if you have a `Foobar` Orm class and override the
-    get_foobar_fields testdata method, then get_foobar() would call get_foobar_fields
-    and get_orm(Foobar, **kwargs) would also call get_foobar_fields. So all of these
-    methods can be mixed and matched
+    get_foobar_fields testdata method, then get_foobar() would call
+    get_foobar_fields and get_orm(Foobar, **kwargs) would also call
+    get_foobar_fields. So all of these methods can be mixed and matched
 
     :Example:
         from prom.extras.testdata import ModelData
 
         class MyModelData(ModelData):
-            def get_foobar_fields(self, orm_class, **kwargs):
-                return super().get_orm_fields(orm_class, **kwargs)
+            async def get_foobar_fields(self, orm_class, **kwargs):
+                return await super().get_orm_fields(orm_class, **kwargs)
 
-    If you'd like to customize the testdata method names, you can set the Orm.model_name
-    or the Orm.models_name class properties
+    If you'd like to customize the testdata method names, you can set the
+    Orm.model_name or the Orm.models_name class properties
 
     :Example:
         from prom import Orm
@@ -94,7 +97,7 @@ def _orm_classes(self):
         by default, we want to ignore any orm classes that are defined in this
         library since they are by definition base classes
 
-        :returns: generator, each orm class
+        :returns: generator[Orm], each orm class
         """
         orm_classes = OrderedSubclasses(classes=Orm.orm_classes.values())
         module_name = ReflectModule(__name__).modroot
@@ -104,8 +107,8 @@ def _orm_classes(self):
                 yield orm_class
 
     def _find_method(self, method_name, default_method=None, **kwargs):
-        """Find the method, this will return the user defined method or the default
-        method if no user defined method exists
+        """Find the method, this will return the user defined method or the
+        default method if no user defined method exists
 
         :param method_name: str, the method name
         :param default_method: callable, the fallback method
@@ -125,11 +128,11 @@ def _get_method(self, orm_class, **kwargs):
         kwargs.setdefault("default_method", self.get_orm)
         return self._find_method(method_name, **kwargs)
 
-    def _get(self, orm_class, **kwargs):
+    async def _get(self, orm_class, **kwargs):
         """Internal dispatcher method for get_orm, this will first try and find
         a get_<ORM-NAME> method and fallback to get_orm"""
         method_name, method = self._get_method(orm_class)
-        return self._run_method(method_name, method, orm_class, **kwargs)
+        return await self._run_method(method_name, method, orm_class, **kwargs)
 
     def _gets_count(self, orm_class, **kwargs):
         """Find how many orm_class should be created
@@ -156,11 +159,11 @@ def _gets_method(self, orm_class, **kwargs):
         kwargs.setdefault("default_method", self.get_orms)
         return self._find_method(method_name, **kwargs)
 
-    def _gets(self, orm_class, **kwargs):
+    async def _gets(self, orm_class, **kwargs):
         """Internal dispatcher method for get_orms, this will first try and find
         a get_<ORM-MODELS-NAME> method and fallback to get_orms"""
         method_name, method = self._gets_method(orm_class)
-        return self._run_method(method_name, method, orm_class, **kwargs)
+        return await self._run_method(method_name, method, orm_class, **kwargs)
 
     def _create_method(self, orm_class, **kwargs):
         """Return the .create_orm() type method for orm_class"""
@@ -168,11 +171,11 @@ def _create_method(self, orm_class, **kwargs):
         kwargs.setdefault("default_method", self.create_orm)
         return self._find_method(method_name, **kwargs)
 
-    def _create(self, orm_class, **kwargs):
-        """Internal dispatcher method for create_orm, this will first try and find
-        a create_<ORM-NAME> method and fallback to create_orm"""
+    async def _create(self, orm_class, **kwargs):
+        """Internal dispatcher method for create_orm, this will first try and
+        find a create_<ORM-NAME> method and fallback to create_orm"""
         method_name, method = self._create_method(orm_class)
-        return self._run_method(method_name, method, orm_class, **kwargs)
+        return await self._run_method(method_name, method, orm_class, **kwargs)
 
     def _creates_method(self, orm_class, **kwargs):
         """Return the .create_orms() type method for orm_class"""
@@ -180,11 +183,11 @@ def _creates_method(self, orm_class, **kwargs):
         kwargs.setdefault("default_method", self.create_orms)
         return self._find_method(method_name, **kwargs)
 
-    def _creates(self, orm_class, **kwargs):
-        """Internal dispatcher method for create_orms, this will first try and find
-        a create_<ORM-MODELS-NAME> method and fallback to create_orm"""
+    async def _creates(self, orm_class, **kwargs):
+        """Internal dispatcher method for create_orms, this will first try and
+        find a create_<ORM-MODELS-NAME> method and fallback to create_orm"""
         method_name, method = self._creates_method(orm_class)
-        return self._run_method(method_name, method, orm_class, **kwargs)
+        return await self._run_method(method_name, method, orm_class, **kwargs)
 
     def _fields_method(self, orm_class, **kwargs):
         """Return the .get_orm_fields() type method for orm_class"""
@@ -192,13 +195,16 @@ def _fields_method(self, orm_class, **kwargs):
         kwargs.setdefault("default_method", self.get_orm_fields)
         return self._find_method(method_name, **kwargs)
 
-    def _fields(self, orm_class, **kwargs):
-        """Internal dispatcher method for get_orm_fields, this will first try and find
-        a get_<ORM-NAME>_fields method and fallback to get_orm_fields"""
+    async def _fields(self, orm_class, **kwargs):
+        """Internal dispatcher method for get_orm_fields, this will first try
+        and find a get_<ORM-NAME>_fields method and fallback to get_orm_fields
+        """
         method_name, method = self._fields_method(orm_class)
-        return self._run_method(method_name, method, orm_class, **kwargs)
+        return await self._run_method(method_name, method, orm_class, **kwargs)
 
     def _run_method(self, method_name, method, orm_class, **kwargs):
+        """This can run both asyncronous and syncronous methods, it's not async
+        but should be awaited when running async methods"""
         kwargs.setdefault("orm_class", orm_class)
         logger.debug(
             "Running {} as {} for orm_class {}".format(
@@ -298,37 +304,40 @@ def __getattr__(self, method_name):
         except AttributeError:
             return super().__getattr__(method_name)
 
-    def unsafe_delete_db(self):
+    async def unsafe_delete_db(self):
         """This will delete all the tables from the db
 
         It relies on a method .ensure_safe_env being defined in the project code
         """
-        if self.ensure_safe_env(): # this method needs to be defined in project code
+        # .ensure_safe_env() method needs to be defined in project code
+        if self.ensure_safe_env():
             for inter in get_interfaces().values():
-                inter.unsafe_delete_tables()
+                await inter.unsafe_delete_tables()
 
-    def unsafe_install_orms(self, modulepaths):
+    async def unsafe_install_orms(self, modulepaths):
         """Go through and install all the Orm subclasses found in the passed in
         module paths
 
-        :param modulepaths: Sequence[str], a list of modpaths (eg ["foo.bar", "che"])
+        :param modulepaths: Sequence[str], a list of modpaths (eg ["foo.bar",
+        "che"])
         """
         # import the module paths to load the Orms into memory
         for modulepath in modulepaths:
             rm = ReflectModule(modulepath)
             m = rm.module() 
 
-        # now go through all the orm classes that have been loaded and install them
+        # now go through all the orm classes that have been loaded and install
+        # them
         seen_table_names = set()
         for orm_class in self._orm_classes():
             for s in orm_class.schema.schemas:
                 if s.table_name not in seen_table_names:
-                    s.orm_class.install()
+                    await s.orm_class.install()
                     seen_table_names.add(s.table_name)
 
     def assure_orm_field_names(self, orm_class, **kwargs):
-        """Field instances can have aliases, in order to allow you to pass in aliases,
-        this will go through kwargs and normalize the field names
+        """Field instances can have aliases, in order to allow you to pass in
+        aliases, this will go through kwargs and normalize the field names
 
         :Example:
             class Foobar(Orm):
@@ -343,11 +352,14 @@ class Foobar(Orm):
         :returns: dict, the normalized kwargs
         """
         schema = orm_class.schema
-        # normalize passed in field names to make sure we correctly find the field's
-        # value if it exists
+        # normalize passed in field names to make sure we correctly find the
+        # field's value if it exists
         for field_name in list(kwargs.keys()):
             if schema.has_field(field_name):
-                kwargs.setdefault(schema.field_name(field_name), kwargs.pop(field_name))
+                kwargs.setdefault(
+                    schema.field_name(field_name),
+                    kwargs.pop(field_name)
+                )
 
         return kwargs
 
@@ -355,9 +367,9 @@ def assure_ref_field_names(self, orm_class, ref_class, **kwargs):
         """Make sure the kwargs destined for orm_class don't impact the kwargs
         that will be used to create a ref_class instance
 
-        Sometimes, we have multiple Orm classes that have the same field names and
-        when those fields get set for orm_class they can cause ref_class to fail
-        so we want to strip those common field names out
+        Sometimes, we have multiple Orm classes that have the same field names
+        and when those fields get set for orm_class they can cause ref_class to
+        fail so we want to strip those common field names out
 
         :param orm_class: Orm, the main orm class
         :param ref_class: Orm, the Orm that orm_class references in some way
@@ -375,29 +387,29 @@ def assure_ref_field_names(self, orm_class, ref_class, **kwargs):
 
         return ref_kwargs
 
-    def assure_orm_refs(self, orm_class, assure_orm_class=None, **kwargs):
-        """When creating an orm, they will often need foreign key values, this will
-        go through any of the foreign key ref fields and create a foreign key if
-        it wasn't included.
+    async def assure_orm_refs(self, orm_class, assure_orm_class=None, **kwargs):
+        """When creating an orm, they will often need foreign key values, this
+        will go through any of the foreign key ref fields and create a foreign
+        key if it wasn't included.
 
-        This is a recursive method, when it finds a ref_class it will assure that
-        ref's foreign keys before handling ref, this way all references all the
-        way down the stack can be generated for the top level so the same refs
-        can propogate all the way down the dependency chain
+        This is a recursive method, when it finds a ref_class it will assure
+        that ref's foreign keys before handling ref, this way all references all
+        the way down the stack can be generated for the top level so the same
+        refs can propogate all the way down the dependency chain
 
         :param orm_class: Orm
-        :param assure_orm_class: Orm|None, the orm that is getting its references
-            assured. If this is None then orm_class is considered the orm that is
-            being checked
+        :param assure_orm_class: Orm|None, the orm that is getting its
+            references assured. If this is None then orm_class is considered the
+            orm that is being checked
         :param **kwargs: orm_class's actual field name value will be checked and
             the ref's orm_class.model_name will be checked
-            * ignore_refs: bool, default False, if True then refs will not be checked,
-                passed in refs will still be set
-            * require_fields: bool, default True, if True then create missing refs,
-                if False, then refs won't be created and so if they are missing
-                their fields will not be populated
-        :returns: dict, the kwargs with ref field_name and ref orm_class.model_name
-            will be included
+            * ignore_refs: bool, default False, if True then refs will not be
+                checked, passed in refs will still be set
+            * require_fields: bool, default True, if True then create missing
+                refs, if False, then refs won't be created and so if they are
+                missing their fields will not be populated
+        :returns: dict, the kwargs with ref field_name and ref
+            orm_class.model_name will be included
         """
         logger.debug(f"Assuring orm refs for orm_class {orm_class.__name__}")
         kwargs = self.assure_orm_field_names(orm_class, **kwargs)
@@ -416,7 +428,7 @@ def assure_orm_refs(self, orm_class, assure_orm_class=None, **kwargs):
 
                 if field_name in kwargs:
                     if ref_field_name not in kwargs:
-                        kwargs[ref_field_name] = ref_class.query.eq_pk(
+                        kwargs[ref_field_name] = await ref_class.query.eq_pk(
                             kwargs[field_name]
                         ).one()
 
@@ -426,16 +438,20 @@ def assure_orm_refs(self, orm_class, assure_orm_class=None, **kwargs):
 
                     else:
                         if not ignore_refs:
-                            if require_fields or field.is_required() or self.yes():
+                            if (
+                                require_fields
+                                or field.is_required()
+                                or self.yes()
+                            ):
                                 # handle all ref_class's refs before we handle
                                 # ref_class
-                                kwargs.update(self.assure_orm_refs(
+                                kwargs.update(await self.assure_orm_refs(
                                     ref_class,
                                     assure_orm_class,
                                     **kwargs
                                 ))
 
-                                kwargs[ref_field_name] = self._create(
+                                kwargs[ref_field_name] = await self._create(
                                     ref_class,
                                     **self.assure_ref_field_names(
                                         assure_orm_class,
@@ -447,7 +463,7 @@ def assure_orm_refs(self, orm_class, assure_orm_class=None, **kwargs):
 
         return kwargs
 
-    def create_orm(self, orm_class, **kwargs):
+    async def create_orm(self, orm_class, **kwargs):
         """create an instance of the orm and save it into the db
 
         :param orm_class: Orm
@@ -455,47 +471,50 @@ def create_orm(self, orm_class, **kwargs):
         :returns: Orm, the orm saved into the db
         """
         kwargs.setdefault("ignore_refs", False)
-        instance = self._get(orm_class, **kwargs)
+        instance = await self._get(orm_class, **kwargs)
         try:
-            instance.save(nest=True)
+            await instance.save(nest=True)
 
         except UniqueError as e:
             logger.warning(" ".join([
                 f"Creating {orm_class.__name__} failed because it exists.",
                 "Fetching the existing instance without updating it",
             ]))
-            instance = instance.requery()
+            instance = await instance.requery()
 
         for k in list(kwargs.keys()):
-            # We want to create any orms with FK references to orm_class if  counts
-            # were passed in
+            # We want to create any orms with FK references to orm_class if
+            # counts were passed in
             if k.endswith("_count"):
                 k_model_name = k[0:-6]
                 k_orm_class = self.get_orm_class(k_model_name)
                 if k_orm_class:
-                    logger.debug("Creating {} {} instances tied to {} instance".format(
-                        kwargs[k],
-                        k_orm_class.__name__,
-                        orm_class.__name__,
-                    ))
+                    logger.debug(
+                        "Creating {} {} instances tied to {} instance".format(
+                            kwargs[k],
+                            k_orm_class.__name__,
+                            orm_class.__name__,
+                        )
+                    )
                     kwargs.setdefault(instance.model_name, instance)
-                    self.create_orms(k_orm_class, **kwargs)
+                    await self.create_orms(k_orm_class, **kwargs)
 
         return instance
 
-    def create_orms(self, orm_class, **kwargs):
+    async def create_orms(self, orm_class, **kwargs):
         """create instances of the orm and save it into the db
 
         :param orm_class: Orm
         :param **kwargs:
             count: int, how many instances you want
             <MODEL_NAME>_count: int, alias for count
-        :returns: list, a list of Orm instances that have all been saved into the db
+        :returns: list, a list of Orm instances that have all been saved into
+            the db
         """
         kwargs.setdefault("ignore_refs", False)
-        instances = self._gets(orm_class, **kwargs)
+        instances = await self._gets(orm_class, **kwargs)
         for instance in instances:
-            instance.save(nest=True)
+            await instance.save(nest=True)
         return instances
 
     def get_orm_class(self, model_name, **kwargs):
@@ -526,7 +545,7 @@ def get_orm_class(self, model_name, **kwargs):
 
         raise ValueError(f"could not find an orm_class for {model_name}")
 
-    def get_orm(self, orm_class, **kwargs):
+    async def get_orm(self, orm_class, **kwargs):
         """get an instance of the orm but don't save it into the db
 
         :param orm_class: Orm
@@ -536,12 +555,12 @@ def get_orm(self, orm_class, **kwargs):
         kwargs.setdefault("ignore_refs", True)
         instance = kwargs.get(orm_class.model_name, None)
         if not instance:
-            fields = self._fields(orm_class, **kwargs)
+            fields = await self._fields(orm_class, **kwargs)
             instance = orm_class(fields)
 
         return instance
 
-    def get_orms(self, orm_class, **kwargs):
+    async def get_orms(self, orm_class, **kwargs):
         """get instances of the orm
 
         :param orm_class: Orm
@@ -557,22 +576,22 @@ def get_orms(self, orm_class, **kwargs):
             # because we need related refs, we will need to create refs if they
             # don't exist
             kwargs.setdefault("ignore_refs", False)
-            kwargs = self.assure_orm_refs(orm_class, **kwargs)
+            kwargs = await self.assure_orm_refs(orm_class, **kwargs)
 
         count = self._gets_count(orm_class, **kwargs)
         for _ in range(count):
-            ret.append(self._get(orm_class, **kwargs))
+            ret.append(await self._get(orm_class, **kwargs))
 
         return ret
 
-    def get_orm_fields(self, orm_class, **kwargs):
+    async def get_orm_fields(self, orm_class, **kwargs):
         """Get the fields of an orm_class
 
         :param orm_class: Orm
         :param **kwargs: the fields found in orm_class.schema
-            * require_fields: bool, default True, this will require that all fields
-                have values even if they aren't required, this does not apply to
-                foreign key references
+            * require_fields: bool, default True, this will require that all
+                fields have values even if they aren't required, this does not
+                apply to foreign key references
             * field_callbacks: dict, the key is the field name and the value is
                 a callable that can take self
         :returns: dict
@@ -580,7 +599,7 @@ def get_orm_fields(self, orm_class, **kwargs):
         ret = {}
         schema = orm_class.schema
         kwargs.setdefault("ignore_refs", True)
-        kwargs = self.assure_orm_refs(orm_class, **kwargs)
+        kwargs = await self.assure_orm_refs(orm_class, **kwargs)
         require_fields = kwargs.get("require_fields", True)
         field_callbacks = kwargs.get("field_callbacks", {})
 
@@ -612,7 +631,12 @@ def get_orm_fields(self, orm_class, **kwargs):
                         ret[field_name] = self.choice(field.choices)
 
                     else:
-                        if cb := getattr(field, "testdata", field_callbacks.get(field_name, None)):
+                        cb = getattr(
+                            field,
+                            "testdata",
+                            field_callbacks.get(field_name, None)
+                        )
+                        if cb:
                             ret[field_name] = cb(self)
 
                         else:
@@ -621,7 +645,9 @@ def get_orm_fields(self, orm_class, **kwargs):
 
                             elif issubclass(field_type, int):
                                 size_info = field.size_info()
-                                ret[field_name] = self.get_posint(size_info["size"])
+                                ret[field_name] = self.get_posint(
+                                    size_info["size"]
+                                )
 
                             elif issubclass(field_type, str):
                                 size_info = field.size_info()
@@ -639,7 +665,9 @@ def get_orm_fields(self, orm_class, **kwargs):
 
                             elif issubclass(field_type, float):
                                 size_info = field.size_info()
-                                ret[field_name] = self.get_posfloat(size_info["size"])
+                                ret[field_name] = self.get_posfloat(
+                                    size_info["size"]
+                                )
 
                             elif issubclass(field_type, datetime.datetime):
                                 ret[field_name] = self.get_past_datetime()
@@ -651,7 +679,9 @@ def get_orm_fields(self, orm_class, **kwargs):
                                 ret[field_name] = str(uuid.uuid4())
 
                             else:
-                                raise ValueError(f"Not sure what to do with {field.type}")
+                                raise ValueError(
+                                    f"Not sure what to do with {field.type}"
+                                )
 
         return ret
 
diff --git a/tests/__init__.py b/tests/__init__.py
index e6efc90..3b3c75e 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,26 +1,16 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-import os
-import sys
-import random
-import string
-import datetime
 import logging
-import decimal
-import tempfile
-from uuid import uuid4
 
-import testdata
-from testdata import TestCase, SkipTest
-
-from prom import query
 from prom.compat import *
-from prom.model import Orm
-from prom.config import Schema, DsnConnection, Field, Index, AutoIncrement
-from prom.interface.base import Interface, Connection
-from prom.interface import get_interfaces, find_environ
-from prom.utils import make_dict
+from prom.exception import InterfaceError
 import prom
+from .testdata import (
+    TestData,
+    TestCase,
+    IsolatedAsyncioTestCase as _IsolatedAsyncioTestCase,
+    SkipTest,
+    InterfaceData, # importing hooks into testdata
+)
 
 
 testdata.basic_logging(
@@ -29,398 +19,80 @@
         #"prom": "ERROR",
         #"prom": "INFO",
         "datatypes": "WARNING",
-    }
+        "dsnparse": "WARNING",
+        "psycopg": "WARNING",
+        "aiosqlite": "WARNING",
+        "asyncio": "WARNING",
+        "testdata": "WARNING",
+    },
+#     format="|".join(['[%(levelname).1s',
+#         '%(asctime)s',
+#         '%(process)d.%(thread)d',
+#         '%(name)s',
+#         '%(filename)s:%(lineno)s] %(message)s',
+#     ])
 )
 
 
 logger = logging.getLogger(__name__)
 
 
-class BaseTestCase(TestCase):
-
-    interfaces = set()
-
-    @classmethod
-    def get_interfaces(cls):
-        """Return all currently configured interfaces in a list"""
-        return list(cls.interfaces)
-
-    @classmethod
-    def setUpClass(cls):
-        """make sure there is a default interface for any class"""
-        for inter in cls.create_environ_interfaces():
-            try:
-                inter.unsafe_delete_tables()
-            except inter.InterfaceError as e:
-                logger.exception(e)
+class IsolatedAsyncioTestCase(_IsolatedAsyncioTestCase):
+    interface_class = None
+    """Set this to an Interface class and only that class will be used to create
+    interfaces
 
-            finally:
-                inter.close()
+    Holds the interface class that should be used. This is set on the actual
+    TestCase and then passed to this class in .setUpClass and removed
+    in .tearDownClass
+    """
+    def setUp(self):
+        self.data.interface_class = self.interface_class
 
     def tearDown(self):
-        self.tearDownClass()
-
-    @classmethod
-    def tearDownClass(cls):
-        for inter in cls.interfaces:
-            inter.close()
-        cls.interfaces = set()
-
-    @classmethod
-    def get_interface(cls):
-        i = cls.create_interface()
-        i.connect()
-        return i
-
-    @classmethod
-    def create_interface(cls):
-        for inter in cls.create_environ_interfaces():
-            return inter
-
-    @classmethod
-    def create_dsn_interface(cls, dsn):
-        conn = DsnConnection(dsn)
-        inter = conn.interface
-        cls.interfaces.add(inter)
-        return inter
-
-    @classmethod
-    def create_environ_connections(cls, dsn_env_name="PROM_TEST_DSN"):
-        """creates all the connections that are defined in the environment under
-        <dsn_env_name>_N where N can be any integer"""
-        found = False
-        if dsn_index := os.environ.get("PROM_TEST_DSN_INDEX", 0):
-            for conn in find_environ(f"{dsn_env_name}_{dsn_index}"):
-                found = True
-                yield conn
-
-        else:
-            for conn in find_environ(dsn_env_name):
-                found = True
-                yield conn
-
-        if not found:
-            raise ValueError("No connection found, set PROM_TEST_DSN")
-
-    @classmethod
-    def create_environ_interfaces(cls):
-        """find any interfaces that match dsn_env_name and yield them"""
-        for conn in cls.create_environ_connections():
-            inter = conn.interface
-            cls.interfaces.add(inter)
-            yield inter
-
-    @classmethod
-    def find_interface(cls, interface_class):
-        for inter in cls.create_environ_interfaces():
-            if isinstance(inter, interface_class):
-                return inter
-
-    def get_table(self, table_name=None, interface=None, **fields_or_indexes):
-        """
-        return an interface and schema for a table in the db
-
-        return -- tuple -- interface, schema
-        """
-        i = interface or self.get_interface()
-        s = self.get_schema(table_name, **fields_or_indexes)
-        i.set_table(s)
-        return i, s
-
-    def get_table_name(self, table_name=None, prefix=""):
-        """return a random table name"""
-        if table_name: return table_name
-        return "{}{}_table".format(
-            prefix,
-            "".join(random.sample(string.ascii_lowercase, random.randint(5, 15)))
-        )
-
-    def get_orm_class(self, table_name=None, prefix="orm_class", **properties):
-        tn = self.get_table_name(table_name, prefix=prefix)
-        parent_class = properties.get("parent_class", Orm)
-
-        properties["table_name"] = tn
-
-        if "interface" not in properties:
-            if "connection_name" not in properties:
-                properties["interface"] = self.get_interface()
-
-        has_field = False
-        for k, v in properties.items():
-            if isinstance(v, Field):
-                has_field = True
-                break
-
-            elif isinstance(v, type) and issubclass(v, Field):
-                has_field = True
-                break
-
-        if not has_field:
-            properties.update({
-                "foo": Field(int, True),
-                "bar": Field(str, True),
-                "ifoobar": Index("foo", "bar"),
-            })
-
-        orm_class = type(
-            String(tn),
-            (parent_class,),
-            properties,
-        )
-
-        return orm_class
-
-    def get_orm(self, table_name=None, prefix="orm", **fields):
-        orm_class = self.get_orm_class(table_name, prefix=prefix)
-        t = orm_class(**fields)
-        return t
-
-    def create_orms(self, table_name=None, count=0, **fields):
-        """Create count orms at table_name with fields
-
-        :returns: Orm class, the Orm class created with table_name
-        """
-        orm_class = self.get_orm_class(table_name, **fields)
-        count = count or testdata.get_int(1, 10)
-        self.insert(orm_class, count)
-        return orm_class
-
-    def create_orm(self, table_name=None, **fields):
-        orm_class = self.get_orm(table_name, **fields)
-        fs = self.get_fields(orm_class.schema)
-        return orm_class.create(fs)
-
-    def find_orm_class(self, v):
-        if issubclass(v, Orm):
-            orm_class = v
-
-        elif isinstance(v, query.Query):
-            orm_class = v.orm_class
-
-        else:
-            orm_class = getattr(v, "orm_class", None)
-            if not orm_class:
-                raise ValueError("Could not find Orm class")
-
-        return orm_class
-
-    def get_schema(self, table_name=None, prefix="schema", **fields_or_indexes):
-        if not fields_or_indexes:
-            fields_or_indexes.setdefault("foo", Field(int, True))
-            fields_or_indexes.setdefault("bar", Field(str, True))
-            fields_or_indexes.setdefault("ifoobar", Index("foo", "bar"))
-
-        fields_or_indexes.setdefault("_id", AutoIncrement())
-
-        # remove any None values
-        for k in list(fields_or_indexes.keys()):
-            if not fields_or_indexes[k]:
-                fields_or_indexes.pop(k)
-
-        s = Schema(
-            self.get_table_name(table_name, prefix=prefix),
-            **fields_or_indexes
-        )
-        return s
+        self.data.interface_class = None
 
-    def create_schema(self, inter=None, table_name=None, **fields_or_indexes):
-        if not inter:
-            inter = self.get_interface()
+    async def asyncSetUp(self):
+        # close any global connections
+        for name, inter in prom.interface.interfaces.items():
+            await inter.close()
 
-        s = self.get_schema(table_name, **fields_or_indexes)
-        inter.set_table(s)
-        return inter, s
+        # we need to delete all the tables
+        inter = self.get_interface()
+        try:
+            await inter.unsafe_delete_tables()
 
-    def find_schema(self, v):
-        if isinstance(v, Schema):
-            schema = v
+        except InterfaceError as e:
+            logger.exception(e)
 
-        elif isinstance(v, query.Query):
-            schema = v.orm_class.schema
+        finally:
+            await inter.close()
 
-        elif issubclass(v, Orm):
-            schema = v.schema
+        # close any test class connections
+        for inter in self.get_interfaces():
+            await inter.close()
 
-        else:
-            schema = getattr(v, "schema", None)
-            if not schema:
-                raise ValueError("Could not find Schema")
+        # discard all the old connections
+        self.interfaces = set()
+        prom.interface.interfaces = {}
 
-        return schema
+    async def asyncTearDown(self):
+        await self.asyncSetUp()
 
-    def get_schema_all(self, inter=None):
-        """return a schema that has a field for all supported standard field types"""
-
-        # this is for foreign key fields
-        orm_class = self.get_orm_class()
-        if inter:
-            orm_class.interface = inter
-            orm_class.install()
-
-        s = Schema(
-            self.get_table_name(),
-            _id=AutoIncrement(),
-            a_bool_y=Field(bool, True),
-            a_bool_n=Field(bool, False),
-            a_sint_y=Field(int, True, size=50),
-            a_sint_n=Field(int, False, size=50),
-            a_dec_y=Field(decimal.Decimal, True),
-            a_dec_n=Field(decimal.Decimal, False),
-            a_float_y=Field(float, True, size=10),
-            a_float_n=Field(float, False, size=10),
-            a_long_y=Field(long, True),
-            a_long_n=Field(long, False),
-            a_fk_y=Field(orm_class, True),
-            a_fk_n=Field(orm_class, False),
-            a_dt_y=Field(datetime.datetime, True),
-            a_dt_n=Field(datetime.datetime, False),
-            a_d_y=Field(datetime.date, True),
-            a_d_n=Field(datetime.date, False),
-            a_int_y=Field(int, True),
-            a_int_n=Field(int, False),
-            a_str_y=Field(str, True),
-            a_str_n=Field(str, False),
-            a_vchar_y=Field(str, True, max_size=512),
-            a_vchar_n=Field(str, False, max_size=512),
-            a_char_y=Field(str, True, size=32),
-            a_char_n=Field(str, False, size=32),
-        )
-
-        if inter:
-            #i = self.get_interface()
-            inter.set_table(s)
-
-        return s
-
-    def get_query(self, table_name=None, prefix="query"):
-        orm_class = self.get_orm_class(table_name, prefix=prefix)
-        return orm_class.query
-
-    def get_fields(self, schema, **field_kwargs):
-        """return the fields of orm with randomized data"""
-        fields = {}
-        for k, v in schema.fields.items():
-            if v.is_pk(): continue
-            if v.is_ref(): continue
-
-            if issubclass(v.interface_type, basestring):
-                fields[k] = testdata.get_words()
-
-            elif issubclass(v.interface_type, bool):
-                fields[k] = True if random.randint(0, 1) == 1 else False
-
-            elif issubclass(v.interface_type, int):
-                fields[k] = testdata.get_int32()
-
-            elif issubclass(v.interface_type, long):
-                fields[k] = testdata.get_int64()
-
-            elif issubclass(v.interface_type, datetime.datetime):
-                fields[k] = testdata.get_past_datetime()
-
-            elif issubclass(v.interface_type, float):
-                fields[k] = testdata.get_float()
-
-            else:
-                raise ValueError("{}".format(v.interface_type))
-
-        fields.update(field_kwargs)
-        return fields
-
-    def insert(self, *args, **kwargs):
-        """most typically you will call this with (object, count)
-
-        :param *args:
-            - interface, schema, count
-            - query, count
-            - (interface, schema), count
-            - orm_class, count
-        :returns: list[Any], a list of primary keys
-        """
-        pks = []
-        if len(args) == 1:
-            o = args[0]
-            count = 1
-
-        elif len(args) == 2:
-            o = args[0]
-            count = args[1]
-
-        elif len(args) == 3:
-            o = (args[0], args[1])
-            count = args[2]
-
-        else:
-            raise ValueError(f"Passed in {len(args)} arguments")
-
-        for i in range(count):
-            pks.append(self.insert_fields(o))
-
-        return pks
-
-    def insert_fields(self, o, fields=None, **fields_kwargs):
-        """Insert fields
-
-        :param o: Query|tuple[Interface, Schema]|orm
-        :param fields: dict|None
-        :param **fields_kwargs: merged with fields
-        :returns: Any, the primary key for the row containing the inserted fields
-        """
-        fields = make_dict(fields, fields_kwargs)
-
-        if isinstance(o, query.Query):
-            schema = o.orm_class.schema
-            pk = self.insert_fields(o.orm_class, fields)
-
-        elif isinstance(o, tuple):
-            interface, schema = o
-            fields = self.get_fields(schema, **fields)
-            pk = interface.insert(schema, fields)
-
-        else:
-            orm_class = None
-            if isinstance(o, Orm):
-                orm_class = type(o)
-                schema = orm_class.schema
-
-            elif issubclass(o, Orm):
-                orm_class = o
-                schema = orm_class.schema
-
-            if orm_class:
-                fields = self.get_fields(schema, **fields)
-                o = orm_class.create(fields)
-                pk = o.pk
-
-            else:
-                raise ValueError("couldn't insert for object {}".format(o))
-
-        pk_name = schema.pk_name
-        if pk_name:
-            assert pk > 0
-
-        return pk
-
-    def insert_orm(self, orm_class, fields=None, **fields_kwargs):
-        pk = self.insert_fields(orm_class, fields, **fields_kwargs)
-        return orm_class.query.eq_pk(pk).one()
-
-
-class EnvironTestCase(BaseTestCase):
-    """This will run all the tests with multple environments (eg, both SQLite and Postgres)"""
-    interface = None
-
-    @classmethod
-    def create_interface(cls):
-        return cls.create_dsn_interface(cls.interface.config.dsn)
 
+class EnvironTestCase(IsolatedAsyncioTestCase):
+    """This will run all the tests with multple environments (eg, run the test
+        with both SQLite and Postgres interfaces)"""
     def run(self, *args, **kwargs):
-        for inter in self.create_environ_interfaces():
-            type(self).interface = inter
+        for interface_class in self.get_environ_interface_classes():
+            type(self).interface_class = interface_class
             super().run(*args, **kwargs)
 
     def countTestCases(self):
-        ret = super().countTestCases()
-        multiplier = len(list(self.create_environ_connections())) # the number of interfaces
+        ret = super().countTestCases() # this is always 1
+
+        # get the number of interfaces
+        multiplier = len(list(self.create_environ_connections()))
+
         return ret * multiplier
 
diff --git a/tests/config_test.py b/tests/config_test.py
index 4321138..9a3c5c2 100644
--- a/tests/config_test.py
+++ b/tests/config_test.py
@@ -1,21 +1,26 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import os
 import datetime
 import decimal
 import math
 
-import testdata
+from datatypes import Enum
 
-from . import BaseTestCase, EnvironTestCase
+from prom.compat import *
 import prom
 from prom.model import Orm
-from prom.config import Schema, Connection, DsnConnection, Index
-from prom.config import Field
-from prom.compat import *
+from prom.config import (
+    Schema,
+    Connection,
+    DsnConnection,
+    Index,
+    Field,
+)
 
+from . import IsolatedAsyncioTestCase, EnvironTestCase
 
-class SchemaTest(BaseTestCase):
+
+class SchemaTest(IsolatedAsyncioTestCase):
     def test_field_index_property(self):
         s = self.get_schema(
             foo=Field(str, index=True)
@@ -24,9 +29,9 @@ def test_field_index_property(self):
         self.assertFalse(s.indexes["foo"].unique)
 
     def test___init__(self):
-        """
-        I had set the class .fields and .indexes attributes to {} instead of None, so you
-        could only ever create one instance of Schema, this test makes sure that's been fixed
+        """I had set the class .fields and .indexes attributes to {} instead of
+        None, so you could only ever create one instance of Schema, this test
+        makes sure that's been fixed
         """
         s = Schema("foo")
         self.assertTrue(isinstance(s.fields, dict))
@@ -160,7 +165,7 @@ def test_field_model_name(self):
         self.assertEqual("o1_id", field_name)
 
 
-class DsnConnectionTest(BaseTestCase):
+class DsnConnectionTest(IsolatedAsyncioTestCase):
     """Any general tests should go here and always use sqlite because that's
     installed with python. If you have SQLite or PostgreSQL specific connection
     config tests those should go into the appropriate insterface ConfigTest
@@ -207,7 +212,7 @@ def test_bad_classpath(self):
             DsnConnection(dsn)
 
 
-class ConnectionTest(BaseTestCase):
+class ConnectionTest(IsolatedAsyncioTestCase):
     def test_interface_is_unique_each_time(self):
         c = Connection(
             interface_name="prom.interface.sqlite.SQLite",
@@ -224,7 +229,6 @@ def test_interface_is_unique_each_time(self):
             inters.add(inter)
 
     def test___init__(self):
-
         c = Connection(
             interface_name="prom.interface.sqlite.SQLite",
             database="dbname",
@@ -238,7 +242,7 @@ def test___init__(self):
 
 
 class FieldTest(EnvironTestCase):
-    def test_modified_pk(self):
+    async def test_modified_pk(self):
         orm_class = self.get_orm_class()
         o = orm_class(foo=1, bar="2")
         f = o.schema.pk
@@ -249,7 +253,7 @@ def test_modified_pk(self):
         im = f.modified(o, None)
         self.assertFalse(im)
 
-        o.save()
+        await o.save()
 
         im = f.modified(o, None)
         self.assertTrue(im)
@@ -271,6 +275,8 @@ def test_help(self):
 
     def test_type_std(self):
         std_types = (
+            str,
+            bytes,
             bool,
             long,
             int,
@@ -280,10 +286,6 @@ def test_type_std(self):
             datetime.datetime,
             datetime.date,
         )
-        if is_py2:
-            std_types = (basestring,) + std_types
-        else:
-            std_types = basestring + std_types
 
         for field_type in std_types:
             f = Field(field_type)
@@ -326,7 +328,7 @@ def test_type_fk(self):
         self.assertIsNotNone(f.schema)
         self.assertFalse(f.is_serialized())
 
-    def test_type_fk_get(self):
+    async def test_type_fk_get(self):
         """ https://github.com/Jaymon/prom/issues/145 """
         class PrimaryKey(Field):
             def __init__(self):
@@ -336,16 +338,24 @@ def iset(self, orm, v):
             def iget(self, orm, v):
                 return int(v)
 
-        foo_class = self.get_orm_class(_id=PrimaryKey(), _created=None, _updated=None)
-        bar_class = self.get_orm_class(foo_id=Field(foo_class), _created=None, _updated=None)
+        foo_class = self.get_orm_class(
+            _id=PrimaryKey(),
+            _created=None,
+            _updated=None
+        )
+        bar_class = self.get_orm_class(
+            foo_id=Field(foo_class),
+            _created=None,
+            _updated=None
+        )
 
-        f = foo_class.create(pk=1)
-        b = bar_class.create(foo_id=f.pk)
+        f = await foo_class.create(pk=1)
+        b = await bar_class.create(foo_id=f.pk)
 
-        foo_pk = bar_class.query.select_foo_id().one_pk(b.pk)
+        foo_pk = await bar_class.query.select_foo_id().eq_pk(b.pk).one()
         self.assertTrue(isinstance(foo_pk, int))
 
-    def test_serialize_lifecycle(self):
+    async def test_serialize_lifecycle(self):
         orm_class = self.get_orm_class(
             foo=Field(dict, False)
         )
@@ -355,10 +365,10 @@ def test_serialize_lifecycle(self):
 
         o.foo = {"bar": 1, "che": "two"}
         self.assertTrue(isinstance(o.foo, dict))
-        o.save()
+        await o.save()
         self.assertTrue(isinstance(o.foo, dict))
 
-        o2 = o.query.eq_pk(o.pk).one()
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertTrue(isinstance(o2.foo, dict))
         self.assertEqual(1, o2.foo["bar"])
         self.assertEqual("two", o2.foo["che"])
@@ -392,7 +402,7 @@ def test_iget(self):
         self.assertTrue(o.foo)
         self.assertTrue(isinstance(o.foo, bool))
 
-    def test_iget_really_big_int(self):
+    async def test_iget_really_big_int(self):
         """Postgres will return Decimal for a really big int, SQLite will return
         an int. I'm not sure it's worth slowing down every select statement to
         check if an int is a decimal since this is such a rare use case but this
@@ -403,8 +413,8 @@ def test_iget_really_big_int(self):
         orm_class = self.get_orm_class(
             foo=Field(int, True, precision=78)
         )
-        o = orm_class.create(foo=int("9" * 77))
-        o2 = orm_class.query.one_pk(o.pk)
+        o = await orm_class.create(foo=int("9" * 77))
+        o2 = await orm_class.query.eq_pk(o.pk).one()
         self.assertTrue(isinstance(o2.foo, (int, decimal.Decimal)))
 
     def test_iset(self):
@@ -430,7 +440,7 @@ class IqueryOrm(Orm):
             def foo(query, v):
                 return 10
 
-        q = IqueryOrm.query.is_foo("foo")
+        q = IqueryOrm.query.eq_foo("foo")
         self.assertEqual(10, q.fields_where[0].value)
 
     def test_datetime_jsonable_1(self):
@@ -457,8 +467,7 @@ class FDefaultOrm(Orm):
         self.assertEqual(None, bar.fdefault(o, None))
         self.assertEqual(None, o.bar)
 
-    def test_fcrud(self):
-
+    async def test_fcrud(self):
         class FCrudOrm(Orm):
             interface = self.get_interface()
             foo = Field(int)
@@ -481,13 +490,13 @@ def foo(self, val):
         self.assertEqual(1, o.foo)
         self.assertEqual(1, o.foo)
 
-        pk = o.save()
+        pk = await o.save()
         self.assertEqual(2, o.foo)
         self.assertEqual(2, o.foo)
 
         del o.foo
         self.assertEqual(None, o.foo)
-        pk = o.save()
+        pk = await o.save()
         self.assertEqual(1, o.foo)
 
         o.foo = 10
@@ -497,7 +506,7 @@ def foo(self, val):
         o.foo = None
         self.assertEqual(1, o.foo)
 
-    def test_icrud(self):
+    async def test_icrud(self):
         class ICrudOrm(Orm):
             interface = self.get_interface()
             foo = Field(int)
@@ -519,16 +528,16 @@ def foo(self, v):
         o = ICrudOrm()
         self.assertEqual(None, o.foo)
 
-        o.save()
+        await o.save()
         self.assertEqual(0, o.foo)
 
         o.foo = 5
         self.assertEqual(5, o.foo)
 
-        o.save()
+        await o.save()
         self.assertEqual(400, o.foo)
 
-        o2 = o.query.one_pk(o.pk)
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(400, o2.foo)
 
     def test_fdel(self):
@@ -584,7 +593,7 @@ def foo(self, val):
         self.assertEqual(None, o.foo)
 
     def test_ref(self):
-        m = testdata.create_module([
+        m = self.create_module([
             "import prom",
             "class Foo(prom.Orm):",
             "    che = prom.Field(str)",
@@ -597,12 +606,16 @@ def test_ref(self):
         Foo = m.module().Foo
         Bar = m.module().Bar
 
-        self.assertTrue(isinstance(Bar.schema.fields['foo_id'].schema, Schema))
-        self.assertTrue(issubclass(Bar.schema.fields['foo_id'].interface_type, long))
+        self.assertTrue(
+            isinstance(Bar.schema.fields['foo_id'].schema, Schema)
+        )
+        self.assertTrue(
+            issubclass(Bar.schema.fields['foo_id'].interface_type, long)
+        )
 
     def test_string_ref(self):
-        modname = testdata.get_module_name()
-        d = testdata.create_modules({
+        modname = self.get_module_name()
+        d = self.create_modules({
             "foo": [
                 "import prom",
                 "class Foo(prom.Orm):",
@@ -623,9 +636,13 @@ def test_string_ref(self):
         Bar = d.module("{}.bar".format(modname)).Bar
 
         self.assertTrue(isinstance(Foo.schema.fields['bar_id'].schema, Schema))
-        self.assertTrue(issubclass(Foo.schema.fields['bar_id'].interface_type, long))
+        self.assertTrue(
+            issubclass(Foo.schema.fields['bar_id'].interface_type, long)
+        )
         self.assertTrue(isinstance(Bar.schema.fields['foo_id'].schema, Schema))
-        self.assertTrue(issubclass(Bar.schema.fields['foo_id'].interface_type, long))
+        self.assertTrue(
+            issubclass(Bar.schema.fields['foo_id'].interface_type, long)
+        )
 
     def test___init__(self):
         f = Field(str, True)
@@ -661,7 +678,8 @@ class bar(Field):
         self.assertEqual("bar", o.bar)
 
     def test_get_size(self):
-        """Makes sure sizes get set correctly, I've evidently had a bug in this for years (1-26-2023)"""
+        """Makes sure sizes get set correctly, I've evidently had a bug in this
+        for years (1-26-2023)"""
         f = Field(int, True, max_size=100)
         self.assertEqual(100, f.options["max_size"])
 
@@ -683,7 +701,8 @@ def test_get_size(self):
         self.assertEqual(500, f.options["max_size"])
 
     def test_size_info_1(self):
-        # A field with precision 65, scale 30 must round to an absolute value less than 10^35
+        # A field with precision 65, scale 30 must round to an absolute value
+        # less than 10^35
         f = Field(float, precision=65, scale=30)
         r = f.size_info()
         self.assertLessEqual(r["size"], math.pow(10, 35))
@@ -746,26 +765,26 @@ def test_default(self):
         o.body["foo"] = 1
         self.assertEqual(1, o.body["foo"])
 
-    def test_imethods_pickle(self):
+    async def test_imethods_pickle(self):
         o = self.get_orm()
         o.body = {"foo": 1}
-        o.save()
+        await o.save()
 
-        o2 = o.requery()
+        o2 = await o.requery()
         self.assertEqual(o.body, o2.body)
 
-    def test_modify(self):
+    async def test_modify(self):
         o = self.get_orm()
         o.body = {"bar": 1}
-        o.save()
+        await o.save()
 
         o.body["che"] = 2
-        o.save()
+        await o.save()
 
-        o2 = o.requery()
+        o2 = await o.requery()
         self.assertEqual(o.body, o2.body)
 
-    def test_other_types(self):
+    async def test_other_types(self):
         types = (
             list,
             set
@@ -774,8 +793,48 @@ def test_other_types(self):
         for field_type in types:
             o = self.get_orm(field_type)
             o.body = field_type(range(100))
-            o.save()
+            await o.save()
 
-            o2 = o.requery()
+            o2 = await o.requery()
             self.assertEqual(o.body, o2.body)
 
+
+class EnumFieldTest(EnvironTestCase):
+    """Moved over and integrated from extras.config on 1-24-2024"""
+    def test_enum(self):
+        class FooEnum(Enum):
+            FOO = 1
+            BAR = 2
+
+        OE = self.get_orm_class(
+            type = Field(FooEnum)
+        )
+
+        o = OE()
+        o.type = "bar"
+        self.assertEqual(FooEnum.BAR, o.type)
+
+        q = o.query.eq_type("foo")
+        self.assertEqual(FooEnum.FOO, q.fields_where[0].value)
+        self.assertEqual(1, q.fields_where[0].value)
+
+        o.type = 2
+        self.assertEqual(FooEnum.BAR, o.type)
+        self.assertEqual(2, o.type)
+
+        o.type = FooEnum.BAR
+        self.assertEqual(FooEnum.BAR, o.type)
+        self.assertEqual(2, o.type)
+
+    async def test_save(self):
+        class FooEnum(Enum):
+            FOO = 1
+
+        OE = self.get_orm_class(type=Field(FooEnum))
+
+        o = await OE.create(type="FOO")
+        f = o.schema.fields["type"]
+        self.assertEqual(FooEnum.FOO.value, o.type)
+        self.assertFalse(f.is_serialized())
+        self.assertTrue(issubclass(f.interface_type, int))
+
diff --git a/tests/extras_test.py b/tests/extras_test.py
index 1cbeee9..24850af 100644
--- a/tests/extras_test.py
+++ b/tests/extras_test.py
@@ -1,35 +1,31 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-
-from datatypes import Enum
 
 from prom.model import Orm
-from prom.extras.config import Field
+from prom.config import Field
 from prom.extras.model import MagicOrm
-from prom.extras.testdata import ModelData
-from . import BaseTestCase, EnvironTestCase, testdata
+from . import IsolatedAsyncioTestCase, EnvironTestCase
 
 
 class MagicOrmTest(EnvironTestCase):
-    def create_1(self, **kwargs):
+    async def create_1(self, **kwargs):
         o1_class = self.get_orm_class(
             bar = Field(bool),
             che = Field(str),
             model_name="o1",
             parent_class=MagicOrm,
         )
-        return self.insert_orm(o1_class, **kwargs)
+        return await self.insert_orm(o1_class, **kwargs)
 
-    def create_2(self, **kwargs):
+    async def create_2(self, **kwargs):
         o1 = self.create_1()
         o2_class = self.get_orm_class(
             o1_id=Field(type(o1)),
             parent_class=MagicOrm,
         )
-        return self.insert_orm(o2_class, **kwargs)
+        return await self.insert_orm(o2_class, **kwargs)
 
-    def test_is(self):
-        o = self.create_1(bar=True, che="che")
+    async def test_is(self):
+        o = await self.create_1(bar=True, che="che")
 
         self.assertTrue(o.is_bar())
         self.assertTrue(o.is_che("che"))
@@ -38,8 +34,8 @@ def test_is(self):
         o.bar = False
         self.assertFalse(o.is_bar())
 
-    def test_jsonable(self):
-        o = self.create_1(_id=500, bar=False, che="1")
+    async def test_jsonable(self):
+        o = await self.create_1(_id=500, bar=False, che="1")
         d = o.jsonable()
         self.assertTrue(o.pk_name in d)
         self.assertFalse("_id" in d)
@@ -64,94 +60,52 @@ def foo(self):
             o.foo
 
 
-class FieldTest(EnvironTestCase):
-    def test_enum(self):
-        class FooEnum(Enum):
-            FOO = 1
-            BAR = 2
-
-        class OE(MagicOrm):
-            type = Field(FooEnum)
-
-        o = OE()
-        o.type = "bar"
-        self.assertEqual(FooEnum.BAR, o.type)
-
-        q = o.query.is_type("foo")
-        self.assertEqual(FooEnum.FOO, q.fields_where[0].value)
-        self.assertEqual(1, q.fields_where[0].value)
-
-        o.type = 2
-        self.assertEqual(FooEnum.BAR, o.type)
-        self.assertEqual(2, o.type)
-
-        o.type = FooEnum.BAR
-        self.assertEqual(FooEnum.BAR, o.type)
-        self.assertEqual(2, o.type)
+class ModelDataTest(IsolatedAsyncioTestCase):
+    @classmethod
+    def setUpClass(cls):
+        # we import this here because it has .get_orm_class and .get_orm
+        # methods that will stomp the normal ones
+        from prom.extras.testdata import ModelData
 
-    def test_save(self):
-        class FooEnum(Enum):
-            FOO = 1
+    @classmethod
+    def tearDownClass(cls):
+        # we remove ModelData here so it doesn't poison other tests
+        from prom.extras.testdata import ModelData
+        cls.data.delete_class(ModelData)
 
-        OE = self.get_orm_class(type=Field(FooEnum))
+    async def test_references_1(self):
+        testdata = self.InterfaceData
+        ref_class = testdata.get_orm_class()
+        orm_class = testdata.get_orm_class(ref_id=Field(ref_class, True))
 
-        o = OE.create(type="FOO")
-        f = o.schema.fields["type"]
-        self.assertEqual(FooEnum.FOO.value, o.type)
-        self.assertFalse(f.is_serialized())
-        self.assertTrue(issubclass(f.interface_type, int))
+        self.assertEqual(0, await ref_class.query.count())
 
+        orm = await self.get_orm(orm_class, ignore_refs=False)
 
-class ModelDataTest(BaseTestCase):
-    def test_model_name(self):
-        pout.b(5)
-        modpath = self.create_module([
-            "from prom import Orm, Field",
-            "",
-            "class Foo(Orm):",
-            "    bar = Field(str)",
-        ])
+        self.assertEqual(1, await ref_class.query.count())
 
-        m = modpath.module()
-
-        foo = self.get_foo()
-        self.assertIsNotNone(foo)
-        self.assertIsInstance(foo, Orm)
-
-        class OtherData(ModelData):
-            def get_bar(self, *args, **kwargs):
-                return self.get_foo()
-
-        d = OtherData()
-        foo2 = d.get_bar()
-        self.assertTrue(type(foo) is type(foo2))
-
-    def test_references_1(self):
-        ref_class = self.get_orm_class()
-        orm_class = self.get_orm_class(ref_id=Field(ref_class, True))
-
-        self.assertEqual(0, ref_class.query.count())
-
-        orm = testdata.get_orm(orm_class, ignore_refs=False)
-
-        self.assertEqual(1, ref_class.query.count())
-
-    def test_references_2(self):
+    async def test_references_2(self):
         count = 2
-        foo_class = self.get_orm_class(model_name="foo")
-        bar_class = self.get_orm_class(foo_id=Field(foo_class))
+        testdata = self.InterfaceData
+        foo_class = testdata.get_orm_class(model_name="foo")
+        bar_class = testdata.get_orm_class(foo_id=Field(foo_class))
 
-        foo = self.insert_orm(foo_class)
+        foo = await self.insert_orm(foo_class)
 
-        bars = self.get_orms(bar_class, count=count, foo=foo, related_refs=False)
+        bars = await self.get_orms(
+            bar_class,
+            count=count,
+            foo=foo,
+            related_refs=False
+        )
 
         bcount = 0
         for b in bars:
             self.assertEqual(foo.pk, b.foo_id)
             bcount += 1
-        self.assertEqual(2, count)
+        self.assertEqual(2, bcount)
 
-    def test_internal_call(self):
+    async def test_internal_call(self):
         """
         https://github.com/Jaymon/prom/issues/166
         """
@@ -166,17 +120,23 @@ def test_internal_call(self):
         ])
 
         m = modpath.module()
+        did_run = {}
 
-        class OtherData(ModelData):
-            def get_foo_fields(s, **kwargs):
-                bar = s.get_bar(**kwargs)
+        class OtherData(self.ModelData.__class__):
+            async def get_foo_fields(s, **kwargs):
+                bar = await s.get_bar(**kwargs)
                 self.assertEqual("Bar", bar.__class__.__name__)
                 self.assertNotIsInstance(bar, kwargs["orm_class"])
+                did_run["get_foo_fields"] = True
 
         d = OtherData()
-        foo = d.get_foo()
+        foo = await d.get_foo() # this method runs the asserts
+        self.assertTrue(did_run["get_foo_fields"])
+
+        # cleanup/remove this class
+        self.data.delete_class(OtherData)
 
-    def test_children_create(self):
+    async def test_children_create(self):
         """
         https://github.com/Jaymon/prom/issues/166
         """
@@ -196,35 +156,43 @@ def test_children_create(self):
 
         count = 2
 
-        foo = self.create_foo(bar_count=count)
+        foo = await self.create_foo(bar_count=count)
         bar_count = 0
-        for b in foo.bars:
+        async for b in await foo.bars:
             bar_count += 1
             self.assertIsInstance(b, m.Bar)
             self.assertEqual(foo.pk, b.foo_id)
         self.assertEqual(count, bar_count)
 
-        foo = self.create_foo(bars_count=count)
+        foo = await self.create_foo(bars_count=count)
         bar_count = 0
-        for b in foo.bars:
+        async for b in await foo.bars:
             bar_count += 1
             self.assertIsInstance(b, m.Bar)
             self.assertEqual(foo.pk, b.foo_id)
         self.assertEqual(count, bar_count)
 
     def test__gets_count(self):
-        orm_class = self.get_orm_class()
+        testdata = self.InterfaceData
+        orm_class = testdata.get_orm_class()
 
+        modeldata = testdata.ModelData
         self.assertEqual(
             2,
-            self._gets_count(orm_class, **{f"{orm_class.model_name}_count": 2})
+            modeldata._gets_count(
+                orm_class,
+                **{f"{orm_class.model_name}_count": 2}
+            )
         )
         self.assertEqual(
             4,
-            self._gets_count(orm_class, **{f"{orm_class.models_name}_count": 4})
+            modeldata._gets_count(
+                orm_class,
+                **{f"{orm_class.models_name}_count": 4}
+            )
         )
         self.assertEqual(
             5,
-            self._gets_count(orm_class, **{"count": 5})
+            modeldata._gets_count(orm_class, **{"count": 5})
         )
 
diff --git a/tests/interface/__init__.py b/tests/interface/__init__.py
index fe56d40..17ff0cf 100644
--- a/tests/interface/__init__.py
+++ b/tests/interface/__init__.py
@@ -1,6 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
-from unittest import TestSuite
 import random
 import string
 import decimal
@@ -15,48 +13,38 @@
 from prom.compat import *
 import prom
 
-from .. import BaseTestCase, testdata
+from .. import (
+    InterfaceData,
+    IsolatedAsyncioTestCase,
+)
 
 
-class _BaseTestConfig(BaseTestCase):
-    """These tests should be for testing config.Connection classes that are
-    specific to a certain interface. Any common config.Connection tests should
-    go into the appropriate config_test class
+class _BaseTestInterface(IsolatedAsyncioTestCase):
+    """This contains tests that are common to all interfaces, it is extended by
+    Postgres's Interface and SQLite's Interface classes
     """
-    def setUp(self):
-        prom.interface.interfaces = {}
-
-    def tearDown(self):
-        prom.interface.interfaces = {}
-
-
-class _BaseTestInterface(BaseTestCase):
-    @classmethod
-    def create_interface(cls):
-        raise NotImplementedError()
-
-    def test_connection___str__(self):
+    async def test_connection___str__(self):
         i = self.get_interface()
-        with i.connection() as conn:
+        async with i.connection() as conn:
             s = f"{conn}"
-            self.assertRegex(s, r"0x[a-f0-9]{12}")
+            self.assertRegex(s, r"0x[a-f0-9]+")
 
-    def test_connect_close(self):
+    async def test_connect_close(self):
         i = self.get_interface()
-        i.close()
-        #self.assertTrue(i.connection is None)
+        await i.connect()
+        await i.close()
         self.assertFalse(i.connected)
 
-    def test_raw_simple(self):
+    async def test_raw_simple(self):
         i = self.get_interface()
-        rows = i.raw('SELECT 1')
+        rows = await i.raw('SELECT 1')
         self.assertGreater(len(rows), 0)
 
-    def test_raw_mismatched_placeholders(self):
-        i, s = self.get_table()
+    async def test_raw_mismatched_placeholders(self):
+        i, s = await self.create_table()
 
         with self.assertRaises(PlaceholderError):
-            i.raw(
+            await i.raw(
                 "SELECT * FROM {} WHERE {} = {} AND {} = {}".format(
                     s,
                     "foo",
@@ -67,30 +55,24 @@ def test_raw_mismatched_placeholders(self):
                 [1]
             )
 
-    def test_transaction_error(self):
-        i = self.get_interface()
-        if is_py2:
-            with self.assertRaises(StopIteration):
-                with i.transaction():
-                    raise StopIteration()
-        else:
-            with self.assertRaises(RuntimeError):
-                with i.transaction():
-                    raise RuntimeError()
-
-    def test_set_table(self):
+    async def test_transaction_error(self):
         i = self.get_interface()
-        s = self.get_schema()
-        r = i.has_table(str(s))
+        with self.assertRaises(RuntimeError):
+            async with i.transaction():
+                raise RuntimeError()
+
+    async def test_set_table_1(self):
+        i, s = self.get_table()
+        r = await i.has_table(str(s))
         self.assertFalse(r)
 
-        r = i.set_table(s)
+        r = await i.set_table(s)
 
-        r = i.has_table(str(s))
+        r = await i.has_table(str(s))
         self.assertTrue(r)
 
         # make sure known indexes are there
-        indexes = i.get_indexes(s)
+        indexes = await i.get_indexes(s)
         count = 0
         for known_index_name, known_index in s.indexes.items():
             for index_name, index_fields in indexes.items():
@@ -101,11 +83,10 @@ def test_set_table(self):
 
         # make sure more exotic datatypes are respected
         s_ref = self.get_schema()
-        i.set_table(s_ref)
-        s_ref_id = self.insert(i, s_ref, 1)[0]
+        await i.set_table(s_ref)
+        s_ref_id = (await self.insert(i, s_ref, 1))
 
-        s = prom.Schema(
-            self.get_table_name(),
+        s = self.get_schema(
             _id=Field(int, autopk=True),
             one=Field(bool, True),
             two=Field(int, True, size=50),
@@ -115,159 +96,189 @@ def test_set_table(self):
             eight=Field(datetime.datetime),
             nine=Field(datetime.date),
         )
-        r = i.set_table(s)
+        r = await i.set_table(s)
         d = {
             'one': True,
             'two': 50,
             'three': decimal.Decimal('1.5'),
             'four': 1.987654321,
             'seven': s_ref_id,
-            'eight': datetime.datetime(2005, 7, 14, 12, 30, tzinfo=datetime.timezone.utc),
+            'eight': datetime.datetime(
+                2005,
+                7,
+                14,
+                12,
+                30,
+                tzinfo=datetime.timezone.utc
+            ),
             'nine': datetime.date(2005, 9, 14),
         }
-        pk = i.insert(s, d)
-        q = query.Query()
-        q.is__id(pk)
-        odb = i.get_one(s, q)
-        #d['five'] = 1.98765
+        pk = await i.insert(s, d)
+        q = Query().eq__id(pk)
+        odb = await i.one(s, q)
         for k, v in d.items():
             self.assertEqual(v, odb[k])
 
-    def test_get_tables(self):
-        i = self.get_interface()
-        s = self.get_schema()
-        r = i.set_table(s)
-        r = i.get_tables()
+    async def test_get_tables(self):
+        i, s = self.get_table()
+        r = await i.set_table(s)
+        r = await i.get_tables()
         self.assertTrue(str(s) in r)
 
-        r = i.get_tables(str(s))
+        r = await i.get_tables(str(s))
         self.assertTrue(str(s) in r)
 
-    def test_get_with_modified_table(self):
+    async def test_get_with_modified_table(self):
         """Make sure SELECT statements with a non-existent field don't fail.
 
         NOTE -- SQLite 3.40.1 is not raising an error on selecting non-existent
             fields, this feels like a change to me
+
+        NOTE -- Psycopg3 also changed how it handles None/NULL values:
+            https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#you-cannot-use-is-s
         """
-        i, s = self.get_table(one=Field(int, True))
+        i, s = await self.create_table(one=Field(int, True))
 
         # Add new column
         s.set_field("two", Field(int, False))
 
         # Test if select query succeeds
-        r = i.get(s, Query().eq_two(None))
+        await i.get(s, Query().eq_two(None))
 
-    def test_unsafe_delete_table(self):
-        i = self.get_interface()
-        s = self.get_schema()
+    async def test_unsafe_delete_table_single(self):
+        i, s = self.get_table()
 
-        r = i.set_table(s)
-        self.assertTrue(i.has_table(str(s)))
+        r = await i.set_table(s)
+        self.assertTrue(await i.has_table(str(s)))
 
-        r = i.unsafe_delete_table(s)
-        self.assertFalse(i.has_table(str(s)))
+        r = await i.unsafe_delete_table(s)
+        self.assertFalse(await i.has_table(str(s)))
 
         # make sure it persists
-        i.close()
+        await i.close()
         i = self.get_interface()
-        self.assertFalse(i.has_table(str(s)))
+        self.assertFalse(await i.has_table(str(s)))
 
-    def test_unsafe_delete_tables(self):
+    async def test_unsafe_delete_tables_nofk(self):
         i = self.get_interface()
         s1 = self.get_schema()
-        i.set_table(s1)
+        await i.set_table(s1)
         s2 = self.get_schema()
-        i.set_table(s2)
+        await i.set_table(s2)
 
-        self.assertTrue(i.has_table(s1))
-        self.assertTrue(i.has_table(s2))
+        self.assertTrue(await i.has_table(s1))
+        self.assertTrue(await i.has_table(s2))
 
-        i.unsafe_delete_tables()
+        await i.unsafe_delete_tables()
 
-        self.assertFalse(i.has_table(s1))
-        self.assertFalse(i.has_table(s2))
+        self.assertFalse(await i.has_table(s1))
+        self.assertFalse(await i.has_table(s2))
 
-    def test_readonly(self):
-        s = self.get_schema()
+    async def test_unsafe_delete_tables_fk(self):
         i = self.get_interface()
-        i.readonly(True)
+
+        s1 = self.get_schema("s1")
+        await i.set_table(s1)
+        s2 = self.get_schema("s2", s1_id=Field(s1, True))
+        await i.set_table(s2)
+
+        self.assertTrue(await i.has_table(s1))
+        self.assertTrue(await i.has_table(s2))
+
+        await i.unsafe_delete_tables()
+
+        self.assertFalse(await i.has_table(s1))
+        self.assertFalse(await i.has_table(s2))
+
+    async def test_readonly(self):
+        i, s = self.get_table()
+        await i.readonly(True)
 
         with self.assertRaises(InterfaceError):
-            i.set_table(s)
-        self.assertFalse(i.has_table(s))
+            await i.set_table(s)
+        self.assertFalse(await i.has_table(s))
 
-        i.readonly(False)
-        i.set_table(s)
-        self.assertTrue(i.has_table(s))
+        await i.readonly(False)
+        await i.set_table(s)
+        self.assertTrue(await i.has_table(s))
 
-    def test_custom_pk_int(self):
-        i, s = self.get_table(_id=Field(int, autopk=True), bar=Field(str))
+    async def test_custom_pk_int(self):
+        i, s = await self.create_table(
+            _id=Field(int, autopk=True),
+            bar=Field(str)
+        )
 
-        pk = i.insert(s, {"bar": "barvalue"})
+        pk = await i.insert(s, {"bar": "barvalue"})
         self.assertLess(0, pk)
 
-        d = i.get_one(s, query.Query().select__id().is__id(pk))
+        d = await i.one(s, Query().select__id().eq__id(pk))
         self.assertEqual(pk, d["_id"])
 
-        pk_custom = testdata.get_int()
-        pk = i.insert(s, {"_id": pk_custom})
+        pk_custom = self.get_int()
+        pk = await i.insert(s, {"_id": pk_custom})
         self.assertEqual(pk_custom, pk)
 
-        d = i.get_one(s, query.Query().select__id().is__id(pk_custom))
+        d = await i.one(s, Query().select__id().eq__id(pk_custom))
         self.assertEqual(pk_custom, d["_id"])
 
-    def test_field_bool(self):
+    async def test_field_bool(self):
         """There was a bug where SQLite boolean field always returned True, this
         tests to make sure that is fixed and it won't happen again"""
-        i, s = self.get_table(bar=Field(bool), che=Field(bool))
-        pk = i.insert(s, {"bar": False, "che": True})
+        i, s = await self.create_table(bar=Field(bool), che=Field(bool))
+        pk = await i.insert(s, {"bar": False, "che": True})
 
-        q = query.Query().is__id(pk)
-        d = dict(i.get_one(s, q))
+        d = dict(await i.one(s, Query().eq__id(pk)))
         self.assertFalse(d["bar"])
         self.assertTrue(d["che"])
 
-    def test_field_dict(self):
-        i, s = self.create_schema(
+    async def test_field_dict(self):
+        i, s = await self.create_table(
             foo=Field(dict)
         )
 
         foo = {"bar": 1, "che": "che 1"}
-        pk = i.insert(s, {"foo": foo})
+        pk = await i.insert(s, {"foo": foo})
 
-        d = i.get_one(s, Query().eq__id(pk))
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(1, d["foo"]["bar"])
         self.assertEqual("che 1", d["foo"].get("che"))
 
-    def test_field_datetime_datatypes(self):
+    async def test_field_datetime_datatypes(self):
         """Makes sure datatypes.Datetime works for the different interfaces"""
-        orm_class = self.get_orm_class(bar=Field(datetime.datetime))
-        o = orm_class.create(bar=Datetime())
+        i, s = await self.create_table(
+            bar=Field(datetime.datetime),
+        )
 
         dt = Datetime()
-        o2 = orm_class.query.lt_bar(dt).one()
-        self.assertEqual(o.bar, o2.bar)
+        pk = await i.insert(s, {"bar": dt})
 
-    def test_field_datetime_string(self):
+        d = await i.one(s, Query().eq_bar(dt))
+        self.assertEqual(dt, d["bar"])
+
+    async def test_field_datetime_string(self):
         """
         https://github.com/Jaymon/prom/issues/84
         """
+        i, s = await self.create_table(
+            bar=Field(datetime.datetime),
+        )
+
         datestr = "2019-10-08 20:18:59.566Z"
-        orm_class = self.get_orm_class(bar=Field(datetime.datetime))
-        o = orm_class(bar=datestr)
-        o.save()
+        pk = await i.insert(s, {"bar": datestr})
 
-        o2 = o.query.eq_pk(o.pk).one()
-        self.assertEqual(Datetime(datestr), o2.bar)
+        d = await i.one(s, Query().eq__id(pk))
+        self.assertEqual(Datetime(datestr), d["bar"])
 
-    def test_field_datetime_iso8601(self):
+    async def test_field_datetime_iso8601(self):
         """make sure ISO 8601 formatted datestamps can be added to the db
 
         NOTE -- postgres actually validates the datestamp on insert, so it will 
             reject bad datestamps on insert while sqlite will gladly insert them
             and then it fails while pulling them out, this is not ideal
         """
-        i, s = self.get_table(bar=Field(datetime.datetime))
+        i, s = await self.create_table(
+            bar=Field(datetime.datetime),
+        )
 
         dts = [
             {
@@ -282,20 +293,6 @@ def test_field_datetime_iso8601(self):
                     "microsecond": 50
                 }
             },
-
-#             { # this fails in postgres
-#                 "input": "1570565939.566850",
-#                 "output": {
-#                     "year": 2019,
-#                     "month": 10,
-#                     "day": 8,
-#                     "hour": 20,
-#                     "minute": 18,
-#                     "second": 59,
-#                     "microsecond": 566850,
-#                 }
-#             },
-
             {
                 "input": "20191008T201859",
                 "output": {
@@ -372,61 +369,68 @@ def test_field_datetime_iso8601(self):
 
         for dt in dts:
             if isinstance(dt["output"], dict):
-                pk = i.insert(s, {"bar": dt["input"]})
-                q = query.Query().is__id(pk)
-                d = dict(i.get_one(s, q))
+                pk = await i.insert(s, {"bar": dt["input"]})
+                d = dict(await i.one(s, Query().eq__id(pk)))
 
                 for k, v in dt["output"].items():
                     self.assertEqual(v, getattr(d["bar"], k))
 
             else:
-                with self.assertRaises((dt["output"], InterfaceError), msg=dt["input"]):
-                    pk = i.insert(s, {"bar": dt["input"]})
-                    q = query.Query().is__id(pk)
-                    i.get_one(s, q)
+                output = (dt["output"], InterfaceError)
+                with self.assertRaises(output, msg=dt["input"]):
+                    pk = await i.insert(s, {"bar": dt["input"]})
+                    i.one(s, Query().eq__id(pk))
+
+    async def test_field_none(self):
+        """Make sure we can query fields using None"""
+        i, s = await self.create_table(
+            one=Field(int, True),
+            two=Field(int, False)
+        )
+
+        pk_eq_null = await i.insert(s, {"one": 1})
+        pk_ne_null = await i.insert(s, {"one": 1, "two": 2})
+
+        r = await i.get(s, Query().eq_two(None))
+        self.assertEqual(1, len(r))
+        self.assertEqual(pk_eq_null, r[0]["_id"])
+
+        r = await i.get(s, Query().ne_two(None))
+        self.assertEqual(1, len(r))
+        self.assertEqual(pk_ne_null, r[0]["_id"])
+
+    async def test_insert_1(self):
+        i, s = await self.create_table()
 
-    def test_insert_1(self):
-        i, s = self.get_table()
         d = {
             'foo': 1,
             'bar': 'this is the value',
         }
 
-        pk = i.insert(s, d)
+        pk = await i.insert(s, d)
         self.assertGreater(pk, 0)
 
-    def test_inserts(self):
-        i, s = self.get_table()
+    async def test_inserts(self):
+        i, s = await self.create_table()
         count = 1000
 
-        self.assertEqual(0, i.count(s, Query()))
+        self.assertEqual(0, await i.count(s, Query()))
         def rows():
             for x in range(count):
-                yield {
-                    "foo": x,
-                    "bar": str(x),
-                }
+                yield (x, f"s{x}")
 
-        i.inserts(
+        await i.inserts(
             s,
             ["foo", "bar"],
             rows(),
         )
 
-        self.assertEqual(count, i.count(s, Query()))
+        self.assertEqual(count, await i.count(s, Query()))
 
-#         with pout.p("transaction multi insert"):
-#             with i.transaction(nest=False) as conn:
-#                 for x in range(1000):
-#                     i.insert(s, {
-#                         "foo": x,
-#                         "bar": str(x),
-#                     })
+    async def test_render_sql_1(self):
+        i, s = self.get_table()
+        q = Query()
 
-    def test_render_sql(self):
-        i = self.get_interface()
-        s = self.get_schema()
-        q = query.Query()
         q.in__id(range(1, 5))
         sql, sql_args = i.render_sql(s, q)
         self.assertTrue('_id' in sql)
@@ -453,107 +457,93 @@ def test_render_sql(self):
         self.assertTrue('222' in sql)
         self.assertTrue('111' in sql)
 
-    def test_get_one(self):
-        i, s = self.get_table()
-        _ids = self.insert(i, s, 2)
+    async def test_one_1(self):
+        i, s = await self.create_table()
+        _ids = await self.insert(i, s, 2)
 
         for _id in _ids:
-            q = query.Query()
-            q.is__id(_id)
-            d = i.get_one(s, q)
+            d = await i.one(s, Query().eq__id(_id))
             self.assertEqual(d[s._id.name], _id)
 
-        q = query.Query()
-        q.is__id(12334342)
-        d = i.get_one(s, q)
+        d = await i.one(s, Query().eq__id(12334342))
         self.assertEqual({}, d)
 
-    def test_get_one_offset(self):
+    async def test_one_offset(self):
         """make sure get_one() works as expected when an offset is set"""
-        i, s = self.get_table()
-        q = query.Query()
-        q.set({
+        i, s = await self.create_table()
+
+        q = Query().set({
             'foo': 1,
             'bar': 'v1',
         })
-        pk = i.insert(s, q.fields_set.fields)
+        pk = await i.insert(s, q.fields_set.fields)
 
-        q = query.Query()
-        q.set({
+        q = Query().set({
             'foo': 2,
             'bar': 'v2',
         })
-        pk2 = i.insert(s, q.fields_set.fields)
+        pk2 = await i.insert(s, q.fields_set.fields)
 
-        q = query.Query()
-        q.desc__id().offset(1)
-        d = i.get_one(s, q)
+        q = Query().desc__id().offset(1)
+        d = await i.one(s, q)
         self.assertEqual(d['_id'], pk)
 
         # just make sure to get expected result if no offset
-        q = query.Query()
-        q.desc__id()
-        d = i.get_one(s, q)
+        q = Query().desc__id()
+        d = await i.one(s, q)
         self.assertEqual(d['_id'], pk2)
 
-        q = query.Query()
-        q.desc__id().offset(2)
-        d = i.get_one(s, q)
+        q = Query().desc__id().offset(2)
+        d = await i.one(s, q)
         self.assertEqual({}, d)
 
-        q = query.Query()
-        q.desc__id().offset(1).limit(5)
-        d = i.get_one(s, q)
+        q = Query().desc__id().offset(1).limit(5)
+        d = await i.one(s, q)
         self.assertEqual(d['_id'], pk)
 
-        q = query.Query()
-        q.desc__id().page(2)
-        d = i.get_one(s, q)
+        q = Query().desc__id().page(2)
+        d = await i.one(s, q)
         self.assertEqual(d['_id'], pk)
 
-        q = query.Query()
-        q.desc__id().page(2).limit(5)
-        d = i.get_one(s, q)
+        q = Query().desc__id().page(2).limit(5)
+        d = await i.one(s, q)
         self.assertEqual({}, d)
 
-    def test_get(self):
-        i, s = self.get_table()
-        _ids = self.insert(i, s, 5)
+    async def test_get_1(self):
+        i, s = await self.create_table()
+        _ids = await self.insert(i, s, 5)
 
-        q = query.Query()
-        q.in__id(_ids)
-        l = i.get(s, q)
+        q = Query().in__id(_ids)
+        l = await i.get(s, q)
         self.assertEqual(len(_ids), len(l))
         for d in l:
             self.assertTrue(d[s._id.name] in _ids)
 
         q.limit(2)
-        l = i.get(s, q)
+        l = await i.get(s, q)
         self.assertEqual(2, len(l))
         for d in l:
             self.assertTrue(d[s._id.name] in _ids)
 
-    def test_get_no_where(self):
+    async def test_get_no_where(self):
         """test get with no where clause"""
-        i, s = self.get_table()
-        _ids = self.insert(i, s, 5)
+        i, s = await self.create_table()
+        _ids = await self.insert(i, s, 5)
 
-        q = None
-        l = i.get(s, q)
+        l = await i.get(s, None)
         self.assertEqual(5, len(l))
 
-    def test_get_pagination(self):
+    async def test_get_pagination_1(self):
         """test get but moving through the results a page at a time to make sure
         limit and offset works"""
-        i, s = self.get_table()
-        _ids = self.insert(i, s, 12)
+        i, s = await self.create_table()
+        _ids = await self.insert(i, s, 12)
 
-        q = query.Query()
-        q.limit(5)
+        q = Query().limit(5)
         count = 0
         for p in range(1, 5):
             q.page(p)
-            l = i.get(s, q)
+            l = await i.get(s, q)
             for d in l:
                 self.assertTrue(d[s._id.name] in _ids)
 
@@ -561,197 +551,161 @@ def test_get_pagination(self):
 
         self.assertEqual(12, count)
 
-    def test_get_pagination_offset_only(self):
+    async def test_get_pagination_offset_only(self):
         offset = 5
-        i, s = self.get_table()
-        _ids = self.insert(i, s, 10)[offset:]
+        i, s = await self.create_table()
+        _ids = (await self.insert(i, s, 10))[offset:]
 
         count = 0
-        q = query.Query().offset(offset)
-        for d in i.get(s, q):
+        q = Query().offset(offset)
+        for d in await i.get(s, q):
             self.assertTrue(d[s._id.name] in _ids)
             count += 1
         self.assertEqual(offset, count)
 
-    def test_count(self):
-        i, s = self.get_table()
+    async def test_count(self):
+        i, s = await self.create_table()
 
         # first try it with no rows
-        q = query.Query()
-        r = i.count(s, q)
+        r = await i.count(s, Query())
         self.assertEqual(0, r)
 
         # now try it with rows
-        _ids = self.insert(i, s, 5)
-        q = query.Query()
-        r = i.count(s, q)
+        _ids = await self.insert(i, s, 5)
+        r = await i.count(s, Query())
         self.assertEqual(5, r)
 
-    def test_delete(self):
+    async def test_delete(self):
         # try deleting with no table
-        i = self.get_interface()
-        s = self.get_schema()
-        q = query.Query().is_foo(1)
-        r = i.delete(s, q)
-
-        return
-
         i, s = self.get_table()
+        r = await i.delete(s, Query().eq_foo(1))
 
         # try deleting with no values in the table
-        q = query.Query()
-        q.is_foo(1)
-        r = i.delete(s, q)
+        r = await i.delete(s, Query().eq_foo(1))
         self.assertEqual(0, r)
 
-        _ids = self.insert(i, s, 5)
+        _ids = await self.insert(i, s, 5)
+        q = Query().in__id(_ids)
 
-        # delete all the inserted values
-        q = query.Query()
-        q.in__id(_ids)
-        l = i.get(s, q)
+        l = await i.get(s, q)
         self.assertEqual(5, len(l))
-        r = i.delete(s, q)
+
+        # delete all the inserted values
+        r = await i.delete(s, q)
         self.assertEqual(5, r)
 
         # verify rows are deleted
-        l = i.get(s, q)
+        l = await i.get(s, q)
         self.assertEqual(0, len(l))
 
         # make sure it stuck
-        i.close()
-        i = self.get_interface()
-        l = i.get(s, q)
+        await i.close()
+        #i = self.get_interface()
+        l = await i.get(s, q)
         self.assertEqual(0, len(l))
 
-    def test_update(self):
-        i, s = self.get_table()
-        q = query.Query()
+    async def test_update(self):
+        i, s = await self.create_table()
         d = {
             'foo': 1,
             'bar': 'value 1',
         }
 
-        pk = i.insert(s, d)
+        pk = await i.insert(s, d)
         self.assertGreater(pk, 0)
 
         d = {
             'foo': 2,
             'bar': 'value 2',
         }
-        q.set(d)
-        q.is__id(pk)
-
-        row_count = i.update(s, d, q)
+        q = Query().set(d).eq__id(pk)
+        row_count = await i.update(s, d, q)
 
         # let's pull it out and make sure it persisted
-        q = query.Query()
-        q.is__id(pk)
-        gd = i.get_one(s, q)
+        gd = await i.one(s, Query().eq__id(pk))
         self.assertEqual(d['foo'], gd['foo'])
         self.assertEqual(d['bar'], gd['bar'])
         self.assertEqual(pk, gd["_id"])
 
-    def test_ref(self):
+    async def test_ref_strong(self):
         i = self.get_interface()
-        table_name_1 = "".join(random.sample(string.ascii_lowercase, random.randint(5, 15)))
-        table_name_2 = "".join(random.sample(string.ascii_lowercase, random.randint(5, 15)))
-
-        s_1 = Schema(
-            table_name_1,
+        s_1 = self.get_schema(
             _id=Field(int, autopk=True),
             foo=Field(int, True)
         )
-        s_2 = Schema(
-            table_name_2,
+        s_2 = self.get_schema(
             _id=Field(int, autopk=True),
             s_pk=Field(s_1, True),
         )
 
-        i.set_table(s_1)
-        i.set_table(s_2)
+        await i.set_table(s_1)
+        await i.set_table(s_2)
 
-        pk1 = i.insert(s_1, {'foo': 1})
+        pk1 = await i.insert(s_1, {'foo': 1})
+        pk2 = await i.insert(s_2, {'s_pk': pk1})
+        q2 = Query().eq__id(pk2)
 
-        pk2 = i.insert(s_2, {'s_pk': pk1})
-
-        q2 = query.Query()
-        q2.is__id(pk2)
         # make sure it exists and is visible
-        r = i.get_one(s_2, q2)
+        r = await i.one(s_2, q2)
         self.assertGreater(len(r), 0)
 
-        q1 = query.Query()
-        q1.is__id(pk1)
-        i.delete(s_1, q1)
+        q1 = Query().eq__id(pk1)
+        await i.delete(s_1, q1)
 
-        r = i.get_one(s_2, q2)
+        r = await i.one(s_2, q2)
         self.assertEqual({}, r)
 
-    def test_weak_ref(self):
+    async def test_ref_weak(self):
         i = self.get_interface()
-        table_name_1 = "".join(random.sample(string.ascii_lowercase, random.randint(5, 15)))
-        table_name_2 = "".join(random.sample(string.ascii_lowercase, random.randint(5, 15)))
-
-        s_1 = Schema(
-            table_name_1,
+        s_1 = self.get_schema(
             _id=Field(int, autopk=True),
             foo=Field(int, True)
         )
-        s_2 = Schema(
-            table_name_2,
+        s_2 = self.get_schema(
             _id=Field(int, autopk=True),
             s_pk=Field(s_1, False),
         )
 
-        i.set_table(s_1)
-        i.set_table(s_2)
+        await i.set_table(s_1)
+        await i.set_table(s_2)
 
-        pk1 = i.insert(s_1, {'foo': 1})
+        pk1 = await i.insert(s_1, {'foo': 1})
+        pk2 = await i.insert(s_2, {'s_pk': pk1})
+        q2 = Query().eq__id(pk2)
 
-        pk2 = i.insert(s_2, {'s_pk': pk1})
-        q2 = query.Query()
-        q2.is__id(pk2)
         # make sure it exists and is visible
-        r = i.get_one(s_2, q2)
+        r = await i.one(s_2, q2)
         self.assertGreater(len(r), 0)
 
-        q1 = query.Query()
-        q1.is__id(pk1)
-        i.delete(s_1, q1)
+        q1 = Query().eq__id(pk1)
+        await i.delete(s_1, q1)
 
-        r = i.get_one(s_2, q2)
+        r = await i.one(s_2, q2)
         self.assertGreater(len(r), 0)
         self.assertIsNone(r['s_pk'])
 
-    def test_handle_error_ref(self):
+    async def test_handle_error_ref(self):
         i = self.get_interface()
-        table_name_1 = self.get_table_name()
-        table_name_2 = self.get_table_name()
-
-        s_1 = Schema(
-            table_name_1,
+        s_1 = self.get_schema(
             _id=Field(int, autopk=True),
             foo=Field(int, True)
         )
-        s_2 = Schema(
-            table_name_2,
+        s_2 = self.get_schema(
             _id=Field(int, autopk=True),
             bar=Field(int, True),
             s_pk=Field(s_1),
         )
 
-        q2 = query.Query()
-        q2.is_bar(1)
-
-        r = i.get_one(s_2, q2)
-        self.assertTrue(i.has_table(table_name_1))
-        self.assertTrue(i.has_table(table_name_2))
+        r = await i.one(s_2, Query().eq_bar(1))
+        self.assertTrue(await i.has_table(s_1.table_name))
+        self.assertTrue(await i.has_table(s_2.table_name))
 
-    def test_get_fields(self):
+    async def test_get_fields_1(self):
         i = self.get_interface()
         s = self.get_schema_all(i)
-        fields = i.get_fields(str(s))
+        await i.set_table(s)
+
+        fields = await i.get_fields(str(s))
 
         for field_name, field in s:
             field2 = fields[field_name]
@@ -761,43 +715,64 @@ def test_get_fields(self):
             else:
                 self.assertEqual(field.interface_type, field2["field_type"])
             self.assertEqual(field.is_pk(), field2["pk"])
-            self.assertEqual(field.required, field2["field_required"], field_name)
+            self.assertEqual(
+                field.required,
+                field2["field_required"],
+                field_name
+            )
 
-    def test__handle_field_error(self):
-        i, s = self.get_table()
+    async def test__handle_field_error(self):
+        """Make sure fields can be added if they can take NULL as a value in
+        the db"""
+        i, s = await self.create_table()
+
+        # this field can't take NULL for a value so adding it should fail
         s.set_field("che", Field(str, True))
-        q = query.Query()
-        q.set({
+        self.assertFalse(await i._handle_field_error(s, e=None))
+
+        s = self.get_schema()
+        await i.set_table(s)
+        # this field can take NULL as a value so adding it should succeed
+        s.set_field("che", Field(str, False))
+        self.assertTrue(await i._handle_field_error(s, e=None))
+
+    async def test_handle_error_column(self):
+        i, s = await self.create_table()
+        s.set_field("che", Field(str, True)) # it's required
+        fields = {
             'foo': 1,
             'bar': 'v1',
             'che': "this field will cause the query to fail",
-        })
+        }
 
-        self.assertFalse(i._handle_field_error(s, e=None))
+        with self.assertRaises(prom.InterfaceError):
+            rd = await i.insert(s, fields)
 
-        s = self.get_schema(table_name=str(s))
-        s.che = str, False
-        self.assertTrue(i._handle_field_error(s, e=None))
+        s = self.get_schema()
+        s.set_field("che", Field(str, False)) # not required
+        pk = await i.insert(s, fields)
+        self.assertLess(0, pk)
 
-    def test_handle_error_subquery(self):
+    async def test_handle_error_subquery(self):
         Foo = self.get_orm_class()
         Bar = self.get_orm_class()
+        i = Foo.interface
 
         bar_q = Bar.query.select_foo()
-        foo_ids = list(Foo.query.select_pk().in_foo(bar_q).values())
+        foo_ids = await i.get(Foo.schema, Foo.query.select_pk().in_foo(bar_q))
         self.assertEqual([], foo_ids) # no error means it worked
 
-    def test_error_unsupported_type(self):
+    async def test_error_unsupported_type(self):
         class Che(object): pass
-        i, s = self.get_table()
+        i, s = await self.create_table()
 
         q = Query().select_foo()
-        q.in_bar([1, 2]).is_che(Che())
+        q.in_bar([1, 2]).eq_che(Che())
 
         with self.assertRaises(PlaceholderError):
-            i.get(s, q)
+            await i.get(s, q)
 
-    def test_create_custom_error(self):
+    async def test_create_custom_error(self):
         i = self.get_interface()
         class CustomError(Exception): pass
 
@@ -805,70 +780,54 @@ class CustomError(Exception): pass
         e2 = i.create_error(e)
         self.assertIsInstance(e2, CustomError)
 
-    def test_handle_error_column(self):
-        i, s = self.get_table()
-        s.set_field("che", Field(str, True)) # it's required
-        fields = {
-            'foo': 1,
-            'bar': 'v1',
-            'che': "this field will cause the query to fail",
-        }
-
-        with self.assertRaises(prom.InterfaceError):
-            rd = i.insert(s, fields)
-
-        s = self.get_schema(table_name=str(s))
-        s.set_field("che", Field(str, False)) # not required so error recovery can fire
-        pk = i.insert(s, fields)
-        self.assertLess(0, pk)
-
-    def test_null_values(self):
-        i = self.get_interface()
-        s = Schema(
-            self.get_table_name(),
-            _id=Field(int, autopk=True),
+    async def test_null_values(self):
+        i, s = await self.create_table(
             foo=Field(int, False),
-            bar=Field(int, False),
+            bar=Field(int, False)
         )
 
         # add one with non NULL foo
-        pk1 = i.insert(s, {"bar": 1, "foo": 2})
+        pk1 = await i.insert(s, {"bar": 1, "foo": 2})
 
         # and one with NULL foo
-        pk2 = i.insert(s, {"bar": 1})
+        pk2 = await i.insert(s, {"bar": 1})
 
-        r = i.get_one(s, query.Query().is_bar(1).is_foo(None))
+        r = await i.one(s, Query().eq_bar(1).eq_foo(None))
         self.assertEqual(pk2, r['_id'])
 
-        r = i.get_one(s, query.Query().is_bar(1).not_foo(None))
+        r = await i.one(s, Query().eq_bar(1).ne_foo(None))
         self.assertEqual(pk1, r['_id'])
 
-    def test_transaction_nested_fail_1(self):
+    async def test_transaction_nested_fail_1(self):
         """make sure 2 new tables in a wrapped transaction work as expected"""
         i = self.get_interface()
+
         s1 = self.get_schema(
-            _id=Field(int, autopk=True),
             foo=Field(int, True)
         )
         s2 = self.get_schema(
-            _id=Field(int, autopk=True),
             bar=Field(int, True),
             s_pk=Field(s1),
         )
 
-        with i.transaction() as connection:
-            pk1 = i.insert(s1, {"foo": 1}, connection=connection)
-            pk2 = i.insert(s2, {"bar": 2, "s_pk": pk1}, connection=connection)
+        async with i.transaction() as connection:
+            pk1 = await i.insert(s1, {"foo": 1}, connection=connection)
+            pk2 = await i.insert(
+                s2,
+                {"bar": 2, "s_pk": pk1},
+                connection=connection
+            )
 
-        r1 = i.get_one(s1, Query().eq__id(pk1))
+        r1 = await i.one(s1, Query().eq__id(pk1))
         self.assertEqual(pk1, r1['_id'])
 
-        r2 = i.get_one(s2, Query().eq__id(pk2))
+        r2 = await i.one(s2, Query().eq__id(pk2))
         self.assertEqual(pk2, r2['_id'])
         self.assertEqual(pk1, r2['s_pk'])
 
-    def test_transaction_nested_fail_2(self):
-        """make sure 2 tables where the first one already exists works in a nested transaction"""
+    async def test_transaction_nested_fail_2(self):
+        """make sure 2 tables where the first one already exists works in a
+        nested transaction"""
         i = self.get_interface()
 
         s1 = self.get_schema(
@@ -881,22 +840,25 @@ def test_transaction_nested_fail_2(self):
             s_pk=Field(s1, True),
         )
 
-        with i.transaction() as connection:
-            pk1 = i.insert(s1, {"foo": 1}, connection=connection)
-            pk2 = i.insert(s2, {"bar": 2, "s_pk": pk1}, connection=connection)
+        async with i.transaction() as connection:
+            pk1 = await i.insert(s1, {"foo": 1}, connection=connection)
+            pk2 = await i.insert(
+                s2,
+                {"bar": 2, "s_pk": pk1},
+                connection=connection
+            )
 
-        r1 = i.get_one(s1, query.Query().is__id(pk1))
+        r1 = await i.one(s1, query.Query().eq__id(pk1))
         self.assertEqual(pk1, r1['_id'])
 
-        r2 = i.get_one(s2, query.Query().is__id(pk1))
+        r2 = await i.one(s2, Query().eq__id(pk1))
         self.assertEqual(pk2, r2['_id'])
         self.assertEqual(r2['s_pk'], pk1)
 
-    def test_transaction_nested_fail_3(self):
-        """make sure 2 tables where the first one already exists works, and second one has 2 refs"""
+    async def test_transaction_nested_fail_3(self):
+        """make sure 2 tables where the first one already exists works, and
+        second one has 2 refs"""
         i = self.get_interface()
-        table_name_1 = "{}_1".format(self.get_table_name())
-        table_name_2 = "{}_2".format(self.get_table_name())
 
         s1 = self.get_schema(
             foo=Field(int, True)
@@ -909,19 +871,19 @@ def test_transaction_nested_fail_3(self):
             s_pk2=Field(s1, True),
         )
 
-        pk1 = i.insert(s1, {"foo": 1})
-        pk2 = i.insert(s1, {"foo": 1})
-        pk3 = i.insert(s2, {"bar": 2, "s_pk": pk1, "s_pk2": pk2})
+        pk1 = await i.insert(s1, {"foo": 1})
+        pk2 = await i.insert(s1, {"foo": 1})
+        pk3 = await i.insert(s2, {"bar": 2, "s_pk": pk1, "s_pk2": pk2})
 
-        r1 = i.get_one(s1, query.Query().is__id(pk1))
+        r1 = await i.one(s1, Query().eq__id(pk1))
         self.assertEqual(r1['_id'], pk1)
 
-        r2 = i.get_one(s2, query.Query().is__id(pk3))
+        r2 = await i.one(s2, Query().eq__id(pk3))
         self.assertEqual(r2['_id'], pk3)
         self.assertEqual(r2['s_pk'], pk1)
         self.assertEqual(r2['s_pk2'], pk2)
 
-    def test_transaction_nested_fail_4(self):
+    async def test_transaction_nested_fail_4(self):
         """ran into a bug where this reared its head and data was lost"""
         i = self.get_interface()
 
@@ -944,206 +906,218 @@ def test_transaction_nested_fail_4(self):
             s_pk=Field(s1, True),
         )
 
-        pk1 = i.insert(s1, {"foo": 1})
-        pk12 = i.insert(s1, {"foo": 12})
+        pk1 = await i.insert(s1, {"foo": 1})
+        pk12 = await i.insert(s1, {"foo": 12})
 
-        self.assertEqual(0, i.count(s2, query.Query()))
+        self.assertEqual(0, await i.count(s2, Query()))
 
-        with i.transaction() as connection:
+        async with i.transaction() as connection:
 
             # create something and put in table 2
-            pk2 = i.insert(s2, {"bar": 2, "s_pk": pk1, "s_pk2": pk12}, connection=connection)
+            pk2 = await i.insert(
+                s2,
+                {"bar": 2, "s_pk": pk1, "s_pk2": pk12},
+                connection=connection
+            )
 
             # now this should cause the stuff to fail
             # it fails on the select because a new transaction isn't started, so 
-            # it just discards all the current stuff and adds the table, had this
-            # been a mod query (eg, insert) it would not have failed, this is fixed
-            # by wrapping selects in a transaction if an active transaction is found
-            pk3 = i.get(s3, Query().eq_s_pk(pk1), connection=connection)
+            # it just discards all the current stuff and adds the table, had
+            # this been a mod query (eg, insert) it would not have failed, this
+            # is fixed by wrapping selects in a transaction if an active
+            # transaction is found
+            pk3 = await i.get(s3, Query().eq_s_pk(pk1), connection=connection)
 
-        self.assertEqual(1, i.count(s2, query.Query()))
+        self.assertEqual(1, await i.count(s2, Query()))
 
-    def test_transaction_connection_1(self):
-        """This is the best test for seeing if transactions are working as expected"""
-        orm_class = self.get_orm_class()
-        i = self.get_interface()
-        conn = i.get_connection()
+    async def test_transaction_connection_1(self):
+        """This is the best test for seeing if transactions are working as
+        expected"""
+        i, s = await self.create_table()
+        conn = await i.get_connection()
 
-        conn.transaction_start(prefix="c1")
+        await conn.transaction_start(prefix="c1")
 
         self.assertIsNotNone(conn.interface)
-        orm_class.create(self.get_fields(orm_class.schema), connection=conn)
+        await i.insert(s, self.get_fields(s), connection=conn)
         self.assertIsNotNone(conn.interface)
 
-        conn.cursor().execute("SELECT true")
-        conn.transaction_start(prefix="c2")
-        conn.cursor().execute("SELECT true")
-        conn.transaction_start(prefix="c3")
-        conn.cursor().execute("SELECT true")
-        conn.transaction_start(prefix="c4")
+        await conn.execute("SELECT true")
+        await conn.transaction_start(prefix="c2")
+        await conn.execute("SELECT true")
+        await conn.transaction_start(prefix="c3")
+        await conn.execute("SELECT true")
+        await conn.transaction_start(prefix="c4")
 
         self.assertIsNotNone(conn.interface)
-        orm_class.create(self.get_fields(orm_class.schema), connection=conn)
+        await i.insert(s, self.get_fields(s), connection=conn)
         self.assertIsNotNone(conn.interface)
 
-        conn.transaction_stop()
-        conn.cursor().execute("SELECT true")
-        conn.transaction_stop()
-        conn.cursor().execute("SELECT true")
-        conn.transaction_stop()
-        conn.cursor().execute("SELECT true")
-        conn.transaction_stop()
-
-    def test_transaction_connection_2(self):
-        """Make sure descendant txs inherit settings from ancestors unless explicitely
-        overridden"""
+        await conn.transaction_stop()
+        await conn.execute("SELECT true")
+        await conn.transaction_stop()
+        await conn.execute("SELECT true")
+        await conn.transaction_stop()
+        await conn.execute("SELECT true")
+        await conn.transaction_stop()
+
+    async def test_transaction_connection_2(self):
+        """Make sure descendant txs inherit settings from ancestors unless
+        explicitely overridden"""
         i = self.get_interface()
-        conn = i.get_connection()
+        conn = await i.get_connection()
 
-        conn.transaction_start(nest=False)
+        await conn.transaction_start(nest=False)
         tx = conn.transaction_current()
         self.assertFalse(tx["ignored"])
         self.assertFalse(tx["nest"])
 
-        conn.transaction_start()
+        await conn.transaction_start()
         tx = conn.transaction_current()
         self.assertTrue(tx["ignored"])
         self.assertFalse(tx["nest"])
 
-        conn.transaction_start(nest=True)
+        await conn.transaction_start(nest=True)
         tx = conn.transaction_current()
         self.assertFalse(tx["ignored"])
         self.assertTrue(tx["nest"])
 
-        conn.transaction_start()
+        await conn.transaction_start()
         tx = conn.transaction_current()
         self.assertFalse(tx["ignored"])
         self.assertTrue(tx["nest"])
 
         # finish the two previous txs that allowed nesting
-        conn.transaction_stop()
-        conn.transaction_stop()
+        await conn.transaction_stop()
+        await conn.transaction_stop()
 
         # a new tx that doesn't set nest should inherit ancestor's setting
-        conn.transaction_start()
+        await conn.transaction_start()
         tx = conn.transaction_current()
         self.assertTrue(tx["ignored"])
         self.assertFalse(tx["nest"])
 
-    def test_transaction_context(self):
-        i = self.get_interface()
-        table_name_1 = "{}_1".format(self.get_table_name())
-        table_name_2 = "{}_2".format(self.get_table_name())
-
+    async def test_transaction_context_1(self):
         # these 2 tables exist before the transaction starts
-        s1 = Schema(
-            table_name_1,
-            _id=Field(int, autopk=True),
+        i, s1 = await self.create_table(
             foo=Field(int, True)
         )
-        i.set_table(s1)
 
-        s2 = Schema(
-            table_name_2,
-            _id=Field(int, autopk=True),
+        i, s2 = await self.create_table(
+            interface=i,
             bar=Field(int, True),
             s_pk=Field(s1, True),
         )
-        i.set_table(s2)
 
         pk1 = 0
         pk2 = 0
 
         try:
-            with i.transaction() as connection:
-                pk1 = i.insert(s1, {"foo": 1}, connection=connection)
-
-                with i.transaction(connection) as connection:
-                    pk2 = i.set(s2, {"bar": 2, "s_pk": pk1}, connection=connection)
+            async with i.transaction() as connection:
+                pk1 = await i.insert(s1, {"foo": 1}, connection=connection)
+
+                async with i.transaction(connection) as connection:
+                    pk2 = await i.set(
+                        s2,
+                        {"bar": 2, "s_pk": pk1},
+                        connection=connection
+                    )
                     raise RuntimeError("testing")
 
         except Exception as e:
             pass
 
-        self.assertEqual(0, i.count(s1, query.Query().is__id(pk1)))
-        self.assertEqual(0, i.count(s2, query.Query().is__id(pk2)))
+        self.assertEqual(0, await i.count(s1, Query().eq__id(pk1)))
+        self.assertEqual(0, await i.count(s2, Query().eq__id(pk2)))
+
+    async def test_transaction_context_manager(self):
+        """make sure the with transaction() context manager works as expected"""
+        i, s = self.get_table()
+
+        async with i.transaction() as connection:
+            fields = self.get_fields(s)
+            _id = await i.insert(s, fields, connection=connection)
+
+        self.assertTrue(_id)
+
+        d = await i.one(s, Query().eq__id(_id))
+        self.assertGreater(len(d), 0)
+
+        with self.assertRaises(RuntimeError):
+            async with i.transaction() as connection:
+                fields = self.get_fields(s)
+                _id = await i.insert(s, fields, connection=connection)
+                raise RuntimeError("this should fail")
+
+        d = await i.one(s, Query().eq__id(_id))
+        self.assertEqual(len(d), 0)
 
-    def test_transaction_no_nest(self):
+    async def test_transaction_no_nest(self):
         """Make sure you can turn off nesting in transactions"""
-        i = self.get_interface()
-        s = self.get_schema()
-        i.set_table(s)
+        i, s = await self.create_table()
 
         try:
-            with i.transaction(nest=False, prefix="trans_no_nest") as conn:
-                i.insert(s, self.get_fields(s))
+            async with i.transaction(nest=False, prefix="tx_no_nest") as conn:
+                await i.insert(s, self.get_fields(s))
 
-                with i.transaction() as conn:
-                    i.insert(s, self.get_fields(s))
+                async with i.transaction() as conn:
+                    await i.insert(s, self.get_fields(s))
                     raise RuntimeError("testing")
 
         except Exception as e:
             pass
 
-        self.assertEqual(0, i.count(s, Query()))
+        self.assertEqual(0, await i.count(s, Query()))
 
-    def test_unique(self):
-        i = self.get_interface()
-        s = self.get_schema()
+    async def test_unique(self):
+        i, s = self.get_table()
         s.set_field("should_be_unique", Field(int, True, unique=True))
-        i.set_table(s)
+        await i.set_table(s)
 
-        d = i.insert(s, {'foo': 1, 'bar': 'v1', 'should_be_unique': 1})
+        d = await i.insert(s, {'foo': 1, 'bar': 'v1', 'should_be_unique': 1})
 
         #with self.assertRaises(prom.InterfaceError):
         with self.assertRaises(prom.UniqueError):
-            d = i.insert(s, {'foo': 2, 'bar': 'v2', 'should_be_unique': 1})
+            d = await i.insert(
+                s,
+                {'foo': 2, 'bar': 'v2', 'should_be_unique': 1}
+            )
 
-    def test_ignore_case_primary_key(self):
-        i = self.get_interface()
-        s = self.get_schema(
+    async def test_ignore_case_primary_key(self):
+        i, s = await self.create_table(
             _id=Field(str, True, ignore_case=True, pk=True),
             foo=Field(int, True),
         )
-        i.set_table(s)
 
-        pk = i.insert(s, {'_id': "FOO", "foo": 1})
+        pk = await i.insert(s, {'_id': "FOO", "foo": 1})
 
         with self.assertRaises(InterfaceError):
-            i.insert(s, {'_id': "foo", "foo": 2})
+            await i.insert(s, {'_id': "foo", "foo": 2})
 
-    def test_ignore_case_field(self):
-        i = self.get_interface()
-        s = self.get_schema(
+    async def test_ignore_case_field(self):
+        i, s = await self.create_table(
             foo=Field(str, True, ignore_case=True),
         )
-        i.set_table(s)
 
-        pk = i.insert(s, {'foo': "FOO"})
-        pk2 = i.insert(s, {'foo': "BAR"})
+        pk = await i.insert(s, {'foo': "FOO"})
+        pk2 = await i.insert(s, {'foo': "BAR"})
 
-        d = i.get_one(s, query.Query().eq_foo("foo"))
+        d = await i.one(s, Query().eq_foo("foo"))
         self.assertEqual(pk, d["_id"])
 
-        d = i.get_one(s, query.Query().eq_foo("baR"))
+        d = await i.one(s, Query().eq_foo("baR"))
         self.assertEqual(pk2, d["_id"])
 
-    def test_ignore_case_index(self):
-        i = self.get_interface()
-        s = Schema(
-            self.get_table_name(),
-            _id=Field(int, autopk=True),
+    async def test_ignore_case_index(self):
+        i, s = await self.create_table(
             foo=Field(str, True, ignore_case=True),
             bar=Field(str, True),
             index_foo=Index('foo', 'bar'),
         )
-        i.set_table(s)
 
         v = 'foo-bar@example.com'
-        d = i.insert(s, {'foo': v, 'bar': 'bar'})
-        q = query.Query()
-        q.is_foo(v)
-        r = i.get_one(s, q)
+        d = await i.insert(s, {'foo': v, 'bar': 'bar'})
+        r = await i.one(s, Query().eq_foo(v))
         self.assertGreater(len(r), 0)
 
         # change the case of v
@@ -1151,296 +1125,125 @@ def test_ignore_case_index(self):
         for x in range(len(v)):
             lv[x] = lv[x].upper()
             qv = "".join(lv)
-            q = query.Query()
-            q.eq_foo(qv)
-            r = i.get_one(s, q)
+            r = await i.one(s, Query().eq_foo(qv))
             self.assertGreater(len(r), 0)
             lv[x] = lv[x].lower()
 
-        d = i.insert(s, {'foo': 'FoO', 'bar': 'bar'})
-        q = query.Query()
-        q.is_foo('foo')
-        r = i.get_one(s, q)
+        d = await i.insert(s, {'foo': 'FoO', 'bar': 'bar'})
+        r = await i.one(s, Query().eq_foo('foo'))
         self.assertGreater(len(r), 0)
         self.assertEqual(r['foo'], 'FoO')
 
-        q = query.Query()
-        q.is_foo('Foo').is_bar('BAR')
-        r = i.get_one(s, q)
+        r = await i.one(s, Query().eq_foo('Foo').eq_bar('BAR'))
         self.assertEqual(len(r), 0)
 
-        q = query.Query()
-        q.is_foo('FoO').is_bar('bar')
-        r = i.get_one(s, q)
+        r = await i.one(s, Query().eq_foo('FoO').eq_bar('bar'))
         self.assertGreater(len(r), 0)
         self.assertEqual(r['foo'], 'FoO')
 
-        d = i.insert(s, {'foo': 'foo2', 'bar': 'bar'})
-        q = query.Query()
-        q.is_foo('foo2')
-        r = i.get_one(s, q)
+        d = await i.insert(s, {'foo': 'foo2', 'bar': 'bar'})
+        r = await i.one(s, Query().eq_foo('foo2'))
         self.assertGreater(len(r), 0)
         self.assertEqual(r['foo'], 'foo2')
 
-    def test_in_sql(self):
-        i, s = self.get_table()
-        _ids = self.insert(i, s, 5)
-
-        q = query.Query()
-        q.in__id(_ids)
-        l = list(i.get(s, q))
+    async def test_in_sql(self):
+        i, s = await self.create_table()
+        _ids = await self.insert(i, s, 5)
 
+        l = list(await i.get(s, Query().in__id(_ids)))
         self.assertEqual(len(l), 5)
 
-    def test_sort_order(self):
-        q = self.get_query()
-        self.insert(q.orm_class.interface, q.orm_class.schema, 10)
-
-        q2 = q.copy()
-
-        foos = list(q2.select_foo().asc__id().values())
-        foos.sort()
-
-        for x in range(2, 9):
-            q3 = q.copy()
-            rows = list(q3.select_foo().asc_foo().limit(1).page(x).values())
-            #pout.v(x, foos[x], rows[0])
-            self.assertEqual(foos[x - 1], rows[0])
-
-            q3 = q.copy()
-            row = q3.select_foo().asc_foo().limit(1).page(x).value()
-            self.assertEqual(foos[x - 1], row)
-
-            q3 = q.copy()
-            row = q3.select_foo().asc_foo().limit(1).page(x).value()
-            self.assertEqual(foos[x - 1], row)
-
-            q3 = q.copy()
-            rows = list(q3.select_foo().in_foo(foos).asc_foo(foos).limit(1).page(x).values())
-            self.assertEqual(foos[x - 1], rows[0])
-
-            q3 = q.copy()
-            row = q3.select_foo().in_foo(foos).asc_foo(foos).limit(1).page(x).value()
-            self.assertEqual(foos[x - 1], row)
-
-        for x in range(1, 9):
-            q3 = q.copy()
-            rows = list(q3.select_foo().asc_foo().limit(x).offset(x).values())
-            #pout.v(x, foos[x], rows[0])
-            self.assertEqual(foos[x], rows[0])
-
-            q3 = q.copy()
-            row = q3.select_foo().asc_foo().limit(x).offset(x).value()
-            self.assertEqual(foos[x], row)
-
-            q3 = q.copy()
-            row = q3.select_foo().asc_foo().limit(x).offset(x).value()
-            self.assertEqual(foos[x], row)
-
-            q3 = q.copy()
-            rows = list(q3.select_foo().in_foo(foos).asc_foo(foos).limit(1).offset(x).values())
-            self.assertEqual(foos[x], rows[0])
-
-            q3 = q.copy()
-            row = q3.select_foo().in_foo(foos).asc_foo(foos).limit(1).offset(x).value()
-            self.assertEqual(foos[x], row)
-
-    def test_sort_list(self):
-        q = self.get_query()
-        self.insert(q.orm_class.interface, q.orm_class.schema, 10)
-
-        q2 = q.copy()
-        foos = list(q2.select_foo().values())
-        random.shuffle(foos)
-
-        q3 = q.copy()
-        rows = list(q3.select_foo().in_foo(foos).asc_foo(foos).values())
-        for i, r in enumerate(rows):
-            self.assertEqual(foos[i], r)
-
-        q4 = q.copy()
-        rfoos = list(reversed(foos))
-        rows = list(q4.select_foo().in_foo(foos).desc_foo(foos).values())
-        for i, r in enumerate(rows):
-            self.assertEqual(rfoos[i], r)
-
-        qb = q.copy()
-        rows = list(qb.in_foo(foos).asc_foo(foos).limit(2).offset(2).get())
-        for i, r in enumerate(rows, 2):
-            self.assertEqual(foos[i], r.foo)
-
-        # now test a string value
-        qb = q.copy()
-        bars = list(qb.select_bar().values())
-        random.shuffle(bars)
-
-        qb = q.copy()
-        rows = list(qb.in_bar(bars).asc_bar(bars).get())
-        for i, r in enumerate(rows):
-            self.assertEqual(bars[i], r.bar)
-
-        # make sure limits and offsets work
-        qb = q.copy()
-        rows = list(qb.in_bar(bars).asc_bar(bars).limit(5).get())
-        for i, r in enumerate(rows):
-            self.assertEqual(bars[i], r.bar)
-
-        qb = q.copy()
-        rows = list(qb.in_bar(bars).asc_bar(bars).limit(2).offset(2).get())
-        for i, r in enumerate(rows, 2):
-            self.assertEqual(bars[i], r.bar)
-
-        # make sure you can select on one row and sort on another
-        qv = q.copy()
-        vs = list(qv.select_foo().select_bar().values())
-        random.shuffle(vs)
-
-        qv = q.copy()
-        rows = list(qv.select_foo().asc_bar((v[1] for v in vs)).values())
-        for i, r in enumerate(rows):
-            self.assertEqual(vs[i][0], r)
-
-    def test_transaction_context_manager(self):
-        """make sure the with transaction() context manager works as expected"""
-        i, s = self.get_table()
-        _id = None
-        with i.transaction() as connection:
-            fields = self.get_fields(s)
-            _id = i.insert(s, fields, connection=connection)
-            #_id = self.insert(i, s, 1, connection=connection)[0]
-
-        self.assertTrue(_id)
-
-        q = query.Query()
-        q.is__id(_id)
-        d = i.get_one(s, q)
-        self.assertGreater(len(d), 0)
-
-        with self.assertRaises(RuntimeError):
-            with i.transaction() as connection:
-                fields = self.get_fields(s)
-                _id = i.insert(s, fields, connection=connection)
-                #_id = self.insert(i, s, 1, connection=connection)[0]
-                raise RuntimeError("this should fail")
-
-        q = query.Query()
-        q.is__id(_id)
-        d = i.get_one(s, q)
-        self.assertEqual(len(d), 0)
-
-    def test_render_date_field_sql(self):
-        """this tests the common date kwargs you can use (in both SQLight and Postgres)
-        if we ever add other backends this might need to be moved out of the general
-        generator test"""
-        i = self.get_interface()
-        s = Schema(
-            self.get_table_name(),
+    async def test_render_date_field_sql(self):
+        """this tests the common date kwargs you can use (in both SQLight and
+        Postgres) if we ever add other backends this might need to be moved out
+        of the general generator test"""
+        i, s = await self.create_table(
             foo=Field(datetime.datetime, True),
-            _id=Field(int, True, autopk=True),
             index_foo=Index('foo'),
         )
-        i.set_table(s)
 
-        pk20 = i.insert(s, {'foo': Datetime(2014, 4, 20)})
-        pk21 = i.insert(s, {'foo': Datetime(2014, 4, 21)})
+        pk20 = await i.insert(s, {'foo': Datetime(2014, 4, 20)})
+        pk21 = await i.insert(s, {'foo': Datetime(2014, 4, 21)})
 
-        q = query.Query()
-        q.is_foo(day=20)
-        d = i.get_one(s, q)
+        d = await i.one(s, Query().eq_foo(day=20))
         self.assertEqual(d['_id'], pk20)
 
-        q = query.Query()
-        q.is_foo(day=21, month=4)
-        d = i.get_one(s, q)
+        d = await i.one(s, Query().eq_foo(day=21, month=4))
         self.assertEqual(d['_id'], pk21)
 
-        q = query.Query()
-        q.is_foo(day=21, month=3)
-        d = i.get_one(s, q)
+        d = await i.one(s, Query().eq_foo(day=21, month=3))
         self.assertFalse(d)
 
-    def test_group_field_name(self):
-        i = self.get_interface()
-        s = Schema(
-            self.get_table_name(),
-            _id=Field(int, True, autopk=True),
+    async def test_group_field_name(self):
+        i, s = await self.create_table(
             group=Field(str, True),
         )
-        i.set_table(s)
 
-        text = testdata.get_words()
-        pk = i.insert(s, {'group': text})
-
-        q = query.Query().is__id(pk)
-        d = dict(i.get_one(s, q))
+        text = self.get_words()
+        pk = await i.insert(s, {'group': text})
+        d = dict(await i.one(s, Query().eq__id(pk)))
         self.assertEqual(text, d["group"])
         self.assertEqual(pk, d["_id"])
 
-    def test_bignumber(self):
-        i = self.get_interface()
-        s = self.get_schema(
+    async def test_bignumber(self):
+        i, s = await self.create_table(
             foo=Field(int, True, max_size=int("9" * 78)),
         )
-        i.set_table(s)
 
         foo = int("5" * 78)
-        pk = i.insert(s, {"foo": foo})
-
-        q = query.Query().is__id(pk)
-        d = i.get_one(s, q)
+        pk = await i.insert(s, {"foo": foo})
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(foo, d["foo"])
 
-    def test_text_size_constraint(self):
+    async def test_text_size_constraint(self):
         # both min and max size
-        i, s = self.create_schema(
+        i, s = await self.create_table(
             foo=Field(str, True, min_size=10, max_size=20),
         )
 
         with self.assertRaises(InterfaceError):
-            i.insert(s, {"foo": testdata.get_ascii(30)})
+            await i.insert(s, {"foo": self.get_ascii(30)})
 
         with self.assertRaises(InterfaceError):
-            i.insert(s, {"foo": testdata.get_ascii(5)})
+            await i.insert(s, {"foo": self.get_ascii(5)})
 
-        pk = i.insert(s, {"foo": testdata.get_ascii(15)})
+        pk = await i.insert(s, {"foo": self.get_ascii(15)})
         self.assertLess(0, pk)
 
         # just size
-        i, s = self.create_schema(
+        i, s = await self.create_table(
             foo=Field(str, True, size=10),
         )
 
         with self.assertRaises(InterfaceError):
-            i.insert(s, {"foo": testdata.get_ascii(20)})
+            await i.insert(s, {"foo": self.get_ascii(20)})
 
         with self.assertRaises(InterfaceError):
-            i.insert(s, {"foo": testdata.get_ascii(5)})
+            await i.insert(s, {"foo": self.get_ascii(5)})
 
-        pk = i.insert(s, {"foo": testdata.get_ascii(10)})
+        pk = await i.insert(s, {"foo": self.get_ascii(10)})
         self.assertLess(0, pk)
 
         # just max size
-        i, s = self.create_schema(
+        i, s = await self.create_table(
             foo=Field(str, True, max_size=10),
         )
 
         with self.assertRaises(InterfaceError):
-            foo = testdata.get_ascii(20)
-            pk = i.insert(s, {"foo": foo})
+            pk = await i.insert(s, {"foo": self.get_ascii(20)})
 
-        foo = testdata.get_ascii(5)
-        pk = i.insert(s, {"foo": foo})
-        d = i.get_one(s, query.Query().is__id(pk))
+        foo = self.get_ascii(5)
+        pk = await i.insert(s, {"foo": foo})
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(foo, d["foo"])
 
-        foo = testdata.get_ascii(10)
-        pk = i.insert(s, {"foo": foo})
-        d = i.get_one(s, query.Query().is__id(pk))
+        foo = self.get_ascii(10)
+        pk = await i.insert(s, {"foo": foo})
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(foo, d["foo"])
 
-
-    def test_upsert_pk(self):
-        i, s = self.create_schema(
+    async def test_upsert_pk(self):
+        i, s = await self.create_table(
             foo=Field(int, True),
             bar=Field(str, True),
             che=Field(str, False),
@@ -1448,28 +1251,28 @@ def test_upsert_pk(self):
         )
 
         d = {"foo": 1, "bar": "bar 1"}
-        pk = i.insert(s, d)
+        pk = await i.insert(s, d)
 
         # makes sure conflict update works as expected
         di = {"_id": pk, "foo": 2, "bar": "bar 2"}
         du = {"foo": 3}
-        pk2 = i.upsert(s, di, du, ["_id"])
+        pk2 = await i.upsert(s, di, du, ["_id"])
         self.assertEqual(pk, pk2)
-        d = i.get_one(s, query.Query().is__id(pk))
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(du["foo"], d["foo"])
         self.assertEqual("bar 1", d["bar"])
 
         # makes sure insert works as expected
         di = {"foo": 3, "bar": "bar 3"}
         du = {"che": "che 3"}
-        pk3 = i.upsert(s, di, du, ["foo", "bar"])
+        pk3 = await i.upsert(s, di, du, ["foo", "bar"])
         self.assertNotEqual(pk, pk3)
-        d = i.get_one(s, query.Query().is__id(pk3))
+        d = await i.one(s, Query().eq__id(pk3))
         self.assertEqual(di["foo"], d["foo"])
         self.assertEqual(di["bar"], d["bar"])
 
-    def test_upsert_index(self):
-        i, s = self.create_schema(
+    async def test_upsert_index(self):
+        i, s = await self.create_table(
             foo=Field(str, True),
             bar=Field(str, True),
             che=Field(str, True, default=""),
@@ -1480,31 +1283,27 @@ def test_upsert_index(self):
         di = {"foo": "1", "bar": "1", "che": "1", "baz": 1}
 
         du = {"baz": 1}
-        pk = i.upsert(s, di, du, ["foo", "bar", "che"])
-        d = i.get_one(s, Query().is__id(pk))
+        pk = await i.upsert(s, di, du, ["foo", "bar", "che"])
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(1, d["baz"])
         self.assertEqual(pk, d["_id"])
 
         du = {"baz": 2}
-        pk = i.upsert(s, di, du, ["foo", "bar", "che"])
-        d = i.get_one(s, Query().is__id(pk))
+        pk = await i.upsert(s, di, du, ["foo", "bar", "che"])
+        d = await i.one(s, Query().eq__id(pk))
         self.assertEqual(2, d["baz"])
         self.assertEqual(pk, d["_id"])
 
-    def test_stacktraces(self):
-        i, s = self.create_schema(
+    async def test_stacktraces(self):
+        i, s = await self.create_table(
             foo=Field(str, True),
         )
 
         try:
-            i.insert(s, {"bar": 10}) # there is no bar field so this should fail
+            # there is no bar field so this should fail
+            await i.insert(s, {"bar": 10})
 
         except InterfaceError as e:
             # we want to make sure we aren't wrapping errors again and again
             self.assertFalse(isinstance(e.e, InterfaceError))
 
-
-# https://docs.python.org/2/library/unittest.html#load-tests-protocol
-def load_tests(loader, tests, pattern):
-    return TestSuite()
-
diff --git a/tests/interface/postgres_test.py b/tests/interface/postgres_test.py
index 3193ca7..2fad2b4 100644
--- a/tests/interface/postgres_test.py
+++ b/tests/interface/postgres_test.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 from unittest import skipIf
 import os
 import datetime
@@ -7,12 +6,6 @@
 import subprocess
 from uuid import UUID
 
-# needed to test prom with greenthreads
-try:
-    import gevent
-except ImportError as e:
-    gevent = None
-
 from prom import query
 from prom.query import Query
 from prom.compat import *
@@ -27,13 +20,19 @@
 import prom
 import prom.interface
 
-from . import _BaseTestInterface, _BaseTestConfig, testdata
+from . import (
+    IsolatedAsyncioTestCase,
+    _BaseTestInterface,
+)
 
 
-@skipIf(PostgreSQL is None, "Skipping Postgres config because dependencies not installed")
-class ConfigTest(_BaseTestConfig):
+@skipIf(
+    PostgreSQL is None,
+    "Skipping Postgres config because dependencies not installed"
+)
+class ConfigTest(IsolatedAsyncioTestCase):
     def test_configure_postgres(self):
-        dsn = 'prom.interface.postgres.PostgreSQL://username:password@localhost/db'
+        dsn = 'prom.interface.postgres.PostgreSQL://uname:passw@localhost/db'
         prom.configure(dsn)
         i = prom.get_interface()
         self.assertTrue(i is not None)
@@ -43,14 +42,18 @@ def test_configure_postgres(self):
         i = prom.get_interface('postgres')
         self.assertTrue(i is not None)
 
-        dsn = 'prom.interface.postgres.BogusSdjaksdfInterface://host/dbname#bogus2'
+        dsn = 'prom.interface.postgres.BogusSdjaInterface://host/dbname#bogus2'
         with self.assertRaises(AttributeError):
             prom.configure(dsn)
 
     def test_dsn(self):
         tests = [
             (
-                "prom.interface.postgres.PostgreSQL://username:password@localhost:5000/database?option=1&var=2#fragment",
+                "".join([
+                    "prom.interface.postgres.PostgreSQL",
+                    "://username:password@localhost:5000",
+                    "/database?option=1&var=2#fragment",
+                ]),
                 {
                     'username': "username",
                     'interface_name': "prom.interface.postgres.PostgreSQL",
@@ -83,7 +86,11 @@ def test_dsn(self):
                 }
             ),
             (
-                "prom.interface.postgres.PostgreSQL://localhost/db3?var1=1&var2=2&var3=true&var4=False#name",
+                "".join([
+                    "prom.interface.postgres.PostgreSQL",
+                    "://localhost",
+                    "/db3?var1=1&var2=2&var3=true&var4=False#name",
+                ]),
                 {
                     'interface_name': "prom.interface.postgres.PostgreSQL",
                     'database': "db3",
@@ -109,230 +116,182 @@ def test_dsn(self):
                 )
 
 
-@skipIf(PostgreSQL is None, "Skipping Postgres test because dependencies not installed")
+@skipIf(
+    PostgreSQL is None,
+    "Skipping Postgres test because dependencies not installed"
+)
 class InterfaceTest(_BaseTestInterface):
-    @classmethod
-    def create_interface(cls):
-        return cls.find_interface(PostgreSQL)
 
-    def test_table_persist(self):
+    interface_class = PostgreSQL
+
+    async def test_table_persist(self):
         i = self.get_interface()
         s = self.get_schema()
-        r = i.has_table(str(s))
+        r = await i.has_table(str(s))
         self.assertFalse(r)
 
-        r = i.set_table(s)
+        r = await i.set_table(s)
 
-        r = i.has_table(str(s))
+        r = await i.has_table(str(s))
         self.assertTrue(r)
 
         # make sure it persists
-        i.close()
+        await i.close()
         i = self.get_interface()
-        self.assertTrue(i.has_table(str(s)))
+        self.assertTrue(await i.has_table(str(s)))
 
-    def test_set_table_postgres(self):
+    async def test_set_table_postgres(self):
         """test some postgres specific things"""
         i = self.get_interface()
-        s = prom.Schema(
-            self.get_table_name(),
+        s = self.get_schema(
             _id=Field(int, autopk=True),
             four=Field(float, True, size=10),
             five=Field(float, True),
             six=Field(int, True, max_size=9000000000),
         )
-        r = i.set_table(s)
+        r = await i.set_table(s)
         d = {
             'four': 1.987654321,
             'five': 1.98765,
             'six': 4000000000,
         }
-        pk = i.insert(s, d)
-        q = Query()
-        q.is__id(pk)
-        odb = i.get_one(s, q)
+        pk = await i.insert(s, d)
+        odb = await i.one(s, Query().eq__id(pk))
         for k, v in d.items():
             self.assertEqual(v, odb[k])
 
-    def test_db_disconnect(self):
-        """make sure interface can recover if the db disconnects mid script execution"""
-        i, s = self.get_table()
-        _id = self.insert(i, s, 1)[0]
-        d = i.get_one(s, Query().eq__id(_id))
-        self.assertLess(0, len(d))
-
-        testdata.restart_service("postgresql")
-
-        d = i.get_one(s, Query().eq__id(_id))
-        self.assertLess(0, len(d))
-
-    def test_no_connection(self):
-        """this will make sure prom handles it gracefully if there is no connection available ever"""
-        postgresql = testdata.stop_service("postgresql", ignore_failure=False)
-        time.sleep(1)
-
-        try:
-            i = self.create_interface()
-            s = self.get_schema()
-            q = Query()
-            with self.assertRaises(prom.InterfaceError):
-                i.get(s, q)
-
-        finally:
-            postgresql.start()
-
-    def test_render_sql_eq(self):
+#     def test_db_disconnect(self):
+#         """make sure interface can recover if the db disconnects mid script
+#         execution"""
+#         self.skip_test("this test does not work with docker")
+# 
+#         i, s = self.get_table()
+#         _id = self.insert(i, s, 1)[0]
+#         d = i.get_one(s, Query().eq__id(_id))
+#         self.assertLess(0, len(d))
+# 
+#         testdata.restart_service("postgresql")
+# 
+#         d = i.get_one(s, Query().eq__id(_id))
+#         self.assertLess(0, len(d))
+# 
+#     def test_no_connection(self):
+#         """this will make sure prom handles it gracefully if there is no
+#         connection available ever"""
+#         self.skip_test("this test does not work with docker")
+# 
+#         postgresql = testdata.stop_service("postgresql", ignore_failure=False)
+#         time.sleep(1)
+# 
+#         try:
+#             i = self.create_interface()
+#             s = self.get_schema()
+#             q = Query()
+#             with self.assertRaises(prom.InterfaceError):
+#                 i.get(s, q)
+# 
+#         finally:
+#             postgresql.start()
+
+    async def test_render_sql_eq(self):
         orm_class = self.get_orm_class(
-            ts=Field(datetime.datetime, True)
+            ts=Field(datetime.datetime, True),
+            interface=self.get_interface()
         )
 
         fstr, fargs = orm_class.query.eq_ts(day=10).render(placeholder=True)
-        self.assertTrue('EXTRACT(DAY FROM "ts") = %s' in fstr)
+        self.assertTrue('EXTRACT(DAY FROM "ts") = %s' in fstr, fstr)
         self.assertEqual(10, fargs[0])
 
-        fstr, fargs = orm_class.query.eq_ts(day=11, hour=12).render(placeholder=True)
-        self.assertTrue('EXTRACT(DAY FROM "ts") = %s AND EXTRACT(HOUR FROM "ts") = %s' in fstr)
+        q = orm_class.query.eq_ts(day=11, hour=12)
+        fstr, fargs = q.render(placeholder=True)
+        self.assertTrue('EXTRACT(DAY FROM "ts") = %s' in fstr, fstr)
+        self.assertTrue('AND EXTRACT(HOUR FROM "ts") = %s' in fstr, fstr)
         self.assertEqual(11, fargs[0])
         self.assertEqual(12, fargs[1])
 
         fstr, fargs = orm_class.query.eq_ts(None).render(placeholder=True)
-        self.assertTrue('"ts" IS %s' in fstr)
+        self.assertTrue('"ts" IS NOT DISTINCT FROM %s' in fstr, fstr)
 
         fstr, fargs = orm_class.query.ne_ts(None).render(placeholder=True)
-        self.assertTrue('"ts" IS NOT %s' in fstr)
+        self.assertTrue('"ts" IS DISTINCT FROM %s' in fstr, fstr)
 
         with self.assertRaises(KeyError):
-            fstr, fargs = orm_class.query.is_ts(bogus=5).render(placeholder=True)
+            q = orm_class.query.eq_ts(bogus=5)
+            fstr, fargs = q.render(placeholder=True)
 
-    def test_render_sql_in(self):
+    async def test_render_sql_in(self):
         orm_class = self.get_orm_class(
-            ts=Field(datetime.datetime, True)
+            ts=Field(datetime.datetime, True),
+            interface=self.get_interface()
         )
 
         fstr, fargs = orm_class.query.in_ts(day=10).render(placeholder=True)
         self.assertTrue('EXTRACT(DAY FROM "ts") IN (%s)' in fstr)
 
-        fstr, fargs = orm_class.query.in_ts(day=[11, 13], hour=12).render(placeholder=True)
-        self.assertTrue('EXTRACT(DAY FROM "ts") IN (%s, %s) AND EXTRACT(HOUR FROM "ts") IN (%s)' in fstr)
+        q = orm_class.query.in_ts(day=[11, 13], hour=12)
+        fstr, fargs = q.render(placeholder=True)
+        self.assertTrue('EXTRACT(DAY FROM "ts") IN (%s, %s)' in fstr, fstr)
+        self.assertTrue('AND EXTRACT(HOUR FROM "ts") IN (%s)' in fstr, fstr)
         self.assertEqual([11, 13, 12], fargs)
 
         with self.assertRaises(KeyError):
-            fstr, fargs = orm_class.query.in_ts(bogus=5).render(placeholder=True)
+            q = orm_class.query.in_ts(bogus=5)
+            fstr, fargs = q.render(placeholder=True)
 
-    def test_no_db_error(self):
+    async def test_no_db_error(self):
         # we want to replace the db with a bogus db error
         i, s = self.get_table()
         config = i.config
         config.database = 'this_is_a_bogus_db_name'
-        i = PostgreSQL(config)
         fields = {
             'foo': 1,
             'bar': 'v1',
         }
         with self.assertRaises(prom.InterfaceError):
-            rd = i.insert(s, fields)
+            rd = await i.insert(s, fields)
 
-    def test_uuid_pk(self):
-        i, s = self.create_schema(
+    async def test_uuid_pk(self):
+        i, s = self.get_table(
             _id=Field(UUID, True, autopk=True),
             foo=Field(int, True),
         )
 
-        pk = i.insert(s, {"foo": 1})
-        self.assertEqual(36, len(pk))
+        pk = await i.insert(s, {"foo": 1})
+        self.assertEqual(36, len(str(pk)))
 
-        q = query.Query().is__id(pk)
-        d = dict(i.get_one(s, q))
+        d = dict(await i.one(s, query.Query().eq__id(pk)))
         self.assertEqual(1, d["foo"])
         self.assertEqual(pk, d["_id"])
 
-    def test_invalid_text_repr(self):
-        orm_class1 = self.get_orm_class(
+    async def test_invalid_text_repr(self):
+        i = self.get_interface()
+        s1 = self.get_schema(
             _id=Field(UUID, True, autopk=True),
+            foo=Field(int),
         )
-        orm_class2 = self.get_orm_class(
-            fk=Field(orm_class1, True),
-            interface=orm_class1.interface,
+        s2 = self.get_schema(
+            fk=Field(s1, True),
         )
 
-        o = orm_class1()
-        o.save()
-
-        o2 = orm_class2(fk=o.pk)
-        o2.save()
+        pk = await i.insert(s1, {"foo": 1})
+        await i.insert(s2, {"fk": pk})
 
-        # make sure psycopg2.errors.InvalidTextRepresentation doesn't get misrepresented
+        # make sure psycopg2.errors.InvalidTextRepresentation doesn't get
+        # misrepresented
         with self.assertRaises(prom.InterfaceError):
-            o3 = orm_class2(fk='foo')
-            o3.save()
+            o3 = await i.insert(s2, {"fk": "foo"})
 
-    def test_get_fields_postgres(self):
-        orm_class = self.get_orm_class(
+    async def test_get_fields_postgres(self):
+        i = self.get_interface()
+        s = self.get_schema(
             _id=Field(UUID, True, pk=True),
             foo=Field(str, True, ignore_case=True),
         )
-        orm_class.install()
 
-        fields = orm_class.interface.get_fields(orm_class.schema)
+        await i.set_table(s)
+
+        fields = await i.get_fields(s)
         self.assertTrue(fields["foo"]["ignore_case"])
         self.assertEqual(UUID, fields["_id"]["field_type"])
 
-
-@skipIf(gevent is None, "Skipping Gevent test because gevent module not installed")
-class XInterfaceGeventTest(InterfaceTest):
-    """this class has an X to start so that it will run last when all tests are run"""
-    @classmethod
-    def setUpClass(cls):
-        import prom.interface.postgres.gevent
-        prom.interface.postgres.gevent.patch_all()
-        super(XInterfacePostgresGeventTest, cls).setUpClass()
-
-    @classmethod
-    def create_interface(cls):
-        orig_url = os.environ["PROM_POSTGRES_DSN"]
-        query_str = '?async=1&pool_maxconn=3&pool_class=prom.interface.postgres.gevent.ConnectionPool'
-        os.environ["PROM_POSTGRES_DSN"] += query_str
-        try:
-            i = super(XInterfacePostgresGeventTest, cls).create_interface()
-
-        finally:
-            os.environ["PROM_POSTGRES_DSN"] = orig_url
-
-        return i
-
-    def test_concurrency(self):
-        i = self.get_interface()
-        start = time.time()
-        for _ in range(4):
-            gevent.spawn(i.query, 'select pg_sleep(1)')
-
-        gevent.wait()
-        stop = time.time()
-        elapsed = stop - start
-        self.assertTrue(elapsed >= 2.0 and elapsed < 3.0)
-
-    def test_concurrent_error_recovery(self):
-        """when recovering from an error in a green thread environment one thread
-        could have added the table while the other thread was asleep, this will
-        test to make sure two threads failing at the same time will both recover
-        correctly"""
-        i = self.get_interface()
-        s = self.get_schema()
-        #i.set_table(s)
-        for x in range(1, 3):
-            gevent.spawn(i.insert, s, {'foo': x, 'bar': str(x)})
-
-        gevent.wait()
-
-        q = Query()
-        r = list(i.get(s, q))
-        self.assertEqual(2, len(r))
-
-    def test_table_recovery(self):
-        i = self.get_interface()
-        s = self.get_schema()
-
-        q = Query()
-        l = i.get(s, q)
-        self.assertEqual([], l)
-
diff --git a/tests/interface/sqlite_test.py b/tests/interface/sqlite_test.py
index 9cc69c3..4568f4f 100644
--- a/tests/interface/sqlite_test.py
+++ b/tests/interface/sqlite_test.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import os
 import datetime
 
@@ -7,20 +6,23 @@
 from prom.interface.sqlite import SQLite
 from prom import query, InterfaceError
 from prom.interface.sqlite import SQLite, DatetimeType
-from prom.interface import configure
+from prom.interface import configure, set_interface, get_interface
 from prom.model import Orm
 from prom.config import Field, DsnConnection
 from prom.compat import *
 from prom.query import Query
 
-from . import _BaseTestInterface, _BaseTestConfig, BaseTestCase, testdata
+from . import (
+    IsolatedAsyncioTestCase,
+    _BaseTestInterface,
+)
 
 
-class ConfigTest(_BaseTestConfig):
+class ConfigTest(IsolatedAsyncioTestCase):
     def test_configure_sqlite(self):
-        dsn = 'prom.interface.sqlite.SQLite:///path/to/db'
-        i = prom.configure(dsn)
-        self.assertTrue(i.config.path)
+        dsn = "prom.interface.sqlite.SQLite:///path/to/db"
+        i = configure(dsn)
+        self.assertEqual("/path/to/db", i.config.database)
 
     def test_dsn(self):
         tests = [
@@ -58,7 +60,11 @@ def test_dsn(self):
                 }
             ),
             (
-                "prom.interface.sqlite.SQLite://:memory:?option=1&var=2#fragment_name",
+                "".join([
+                    "prom.interface.sqlite.SQLite",
+                    "://:memory:",
+                    "?option=1&var=2#fragment_name",
+                ]),
                 {
                     'interface_name': "prom.interface.sqlite.SQLite",
                     'host': ":memory:",
@@ -103,7 +109,11 @@ def test_dsn(self):
                 }
             ),
             (
-                "prom.interface.sqlite.SQLite:///abs/path/to/db/4.sqlite?var1=1&var2=2",
+                "".join([
+                    "prom.interface.sqlite.SQLite",
+                    ":///abs/path/to/db/4.sqlite",
+                    "?var1=1&var2=2",
+                ]),
                 {
                     'interface_name': "prom.interface.sqlite.SQLite",
                     'host': None,
@@ -115,7 +125,11 @@ def test_dsn(self):
                 }
             ),
             (
-                "prom.interface.sqlite.SQLite:///abs/path/to/db/4.sqlite?var1=1&var2=2#name",
+                "".join([
+                    "prom.interface.sqlite.SQLite",
+                    ":///abs/path/to/db/4.sqlite",
+                    "?var1=1&var2=2#name",
+                ]),
                 {
                     'interface_name': "prom.interface.sqlite.SQLite",
                     'host': None,
@@ -124,7 +138,11 @@ def test_dsn(self):
                 }
             ),
             (
-                "prom.interface.sqlite.SQLite:///abs/path/to/db/4.sqlite?var1=1&var2=2#name",
+                "".join([
+                    "prom.interface.sqlite.SQLite",
+                    ":///abs/path/to/db/4.sqlite",
+                    "?var1=1&var2=2#name",
+                ]),
                 {
                     'interface_name': "prom.interface.sqlite.SQLite",
                     'host': None,
@@ -148,204 +166,206 @@ def test_dsn(self):
                 )
 
 
-class DatetimeTypeTest(BaseTestCase):
+class DatetimeTypeTest(IsolatedAsyncioTestCase):
     def test_convert(self):
         s = "2020-03-25T19:34:05.00005Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(50, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.000050Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.000050Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
         s = "2020-03-25T19:34:05.05Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(50000, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.050000Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.050000Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
         s = "2020-03-25T19:34:05.0506Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(50600, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.050600Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.050600Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
         s = "2020-03-25T19:34:05.050060Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(50060, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.050060Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.050060Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
         s = "2020-03-25T19:34:05.000057Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(57, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.000057Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.000057Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
         s = "2020-03-25T19:34:05.057Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(57000, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.057000Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.057000Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
         s = "2020-03-25T19:34:05.057035Z"
         dt = DatetimeType.convert(s)
         self.assertEqual(57035, dt.microsecond)
-        self.assertEqual("2020-03-25T19:34:05.057035Z", dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ"))
+        self.assertEqual(
+            "2020-03-25T19:34:05.057035Z",
+            dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
+        )
 
 
 class InterfaceTest(_BaseTestInterface):
-    @classmethod
-    def create_interface(cls):
-        return cls.find_interface(SQLite)
-
-    def test_change_interface(self):
-        """This is testing the actual interface, not the db connection or anything"""
-        class InterTorm(Orm):
-            connection_name = "change-interface"
-            #connection_name = ""
-            pass
-
-        path = testdata.get_file("inter1.db")
-        dsn = "sqlite://{}#{}".format(path, InterTorm.connection_name)
-        configure(dsn)
-        InterTorm.install()
-        self.assertTrue(InterTorm.interface.has_table(InterTorm.table_name))
 
-        path = testdata.get_file("inter2.db")
-        dsn = "sqlite://{}#{}".format(path, InterTorm.connection_name)
+    interface_class = SQLite
+
+    async def test_change_interface(self):
+        """This is testing the actual interface, not the db connection or
+        anything"""
+        connection_name = "change-interface"
+        path = self.get_file("inter1.db")
+        dsn = "sqlite://{}#{}".format(path, connection_name)
         configure(dsn)
-        self.assertFalse(InterTorm.interface.has_table(InterTorm.table_name))
 
-    def test_field_datetime_type(self):
-        s = self.get_schema(
-            self.get_table_name(),
-            foo=Field(datetime.datetime)
+        interface = get_interface(connection_name)
+        schema = self.get_schema()
+
+        await interface.set_table(schema)
+        self.assertTrue(
+            await interface.has_table(schema)
+        )
+        self.assertTrue(interface.is_connected())
+
+        path = self.get_file("inter2.db")
+        dsn = "sqlite://{}#{}".format(path, connection_name)
+
+        with self.assertRaises(ValueError):
+            configure(dsn)
+
+        await interface.close()
+        configure(dsn)
+        interface = get_interface(connection_name)
+        self.assertFalse(
+            await interface.has_table(schema)
         )
-        i = self.create_interface()
-        i.set_table(s)
-
-        foo = testdata.get_past_datetime().astimezone(datetime.timezone.utc)
-        pk = i.insert(s, {"foo": foo})
-        r = i.get_one(s, query.Query().eq__id(pk))
-        self.assertEqual(foo, r["foo"])
-
-    def test_get_fields_float(self):
-        sql = "\n".join([
-            "CREATE TABLE ZFOOBAR (",
-            "ZPK INTEGER PRIMARY KEY,",
-            "ZINTEGER INTEGER,",
-            "ZFLOAT FLOAT,",
-            "ZTIMESTAMP TIMESTAMP,",
-            "ZVARCHAR VARCHAR)",
-        ])
-
-        i = self.create_interface()
-        r = i.raw(sql, cursor_result=True)
-        self.assertTrue(i.has_table("ZFOOBAR"))
-
-        fields = i.get_fields("ZFOOBAR")
-        self.assertEqual(float, fields["ZFLOAT"]["field_type"])
 
-    def test_create_path(self):
-        i = self.create_interface()
+    async def test_create_path(self):
+        i = self.get_interface()
         config = i.config
 
-        d = testdata.create_dir()
+        d = self.create_dir()
         config.host = os.path.join(d, "create_path", "db.sqlite")
 
-        i.connect(config)
-        self.assertTrue(i.connected)
+        await i.connect(config)
+        self.assertTrue(i.is_connected())
 
-    def test_db_disconnect_1(self):
-        """make sure interface can recover if the db disconnects mid script execution,
-        SQLite is a bit different than postgres which is why this method is completely
-        original"""
-        i, s = self.get_table()
-        _id = self.insert(i, s, 1)[0]
-        d = i.get_one(s, Query().eq__id(_id))
+    async def test_db_disconnect_1(self):
+        """make sure interface can recover if the db disconnects mid script
+        execution, SQLite is a bit different than postgres which is why this
+        method is completely original"""
+        i, s = await self.create_table()
+
+        _id = (await self.insert(i, s, 1))
+        d = await i.one(s, Query().eq__id(_id))
         self.assertGreater(len(d), 0)
 
-        i._connection.close()
+        await i._connection.close()
+        self.assertTrue(i.is_connected())
 
-        _id = self.insert(i, s, 1)[0]
-        d = i.get_one(s, Query().eq__id(_id))
+        _id = (await self.insert(i, s, 1))
+        d = await i.one(s, Query().eq__id(_id))
         self.assertGreater(len(d), 0)
 
-    def test_db_disconnect_2(self):
+    async def test_db_disconnect_2(self):
         i = self.get_interface()
-        def callback(connection, **kwargs):
+        async def callback(connection, **kwargs):
             if getattr(connection, "attempt", False):
-                connection.close()
+                await connection.close()
                 connection.attempt = False
-            connection.cursor().execute("SELECT true")
+            (await connection.cursor()).execute("SELECT true")
 
-        with i.connection() as connection:
+        async with i.connection() as connection:
             connection.attempt = True
-            i.execute(callback, connection=connection)
+            await i.execute(callback, connection=connection)
 
-        with i.connection() as connection:
-            i.execute(callback, connection=connection)
+        async with i.connection() as connection:
+            await i.execute(callback, connection=connection)
 
-    def test_unsafe_delete_table_strange_name(self):
-        """this makes sure https://github.com/firstopinion/prom/issues/47 is fixed,
-        the problem was table names weren't escaped and so if they
+    async def test_unsafe_delete_table_strange_name(self):
+        """this makes sure https://github.com/firstopinion/prom/issues/47 is
+        fixed, the problem was table names weren't escaped and so if they
         started with a number or something like that SQLite would choke"""
-        table_name = "1{}".format(testdata.get_ascii(32))
-        i = self.create_interface()
-        s = self.get_schema(table_name)
-        self.insert(i, s, 5)
-        r = i.count(s)
+        table_name = "1{}".format(self.get_ascii(32))
+        i, s = await self.create_table(table_name=table_name)
+        await self.insert(i, s, 5)
+
+        r = await i.count(s)
         self.assertEqual(5, r)
 
-        i.unsafe_delete_table(table_name)
+        await i.unsafe_delete_table(table_name)
 
-        r = i.count(s)
+        r = await i.count(s)
         self.assertEqual(0, r)
 
-        i._delete_table(table_name)
-        self.assertFalse(i.has_table(table_name))
-
-        i.unsafe_delete_tables()
-        self.assertFalse(i.has_table(table_name))
-
-        i.unsafe_delete_tables()
-        self.assertFalse(i.has_table(table_name))
-
-    def test_unsafe_delete_table_ref(self):
-        inter = self.get_interface()
-        m = testdata.create_module([
-            "import prom",
-            "",
-            "class Foo(prom.Orm):",
-            "    table_name = 'dtref_foo'",
-            "",
-            "class Bar(prom.Orm):",
-            "    table_name = 'dtref_bar'",
-            "    foo_id=prom.Field(Foo, True)",
-            "    foo2_id=prom.Field(Foo, True)",
-            ""
-        ])
-
-        Foo = m.module().Foo
-        Foo.interface = inter
-        Bar = m.module().Bar
-        Bar.interface = inter
-
-        Foo.install()
-        Bar.install()
-
-        self.assertTrue(Foo.interface.has_table("dtref_foo"))
-        Foo.interface.unsafe_delete_table("dtref_foo")
-        self.assertFalse(Foo.interface.has_table("dtref_foo"))
-
-        Bar.interface.close()
-        self.assertFalse(Bar.interface.is_connected())
-        self.assertTrue(Bar.interface.has_table("dtref_bar"))
-        Bar.interface.unsafe_delete_tables()
-        self.assertFalse(Bar.interface.has_table("dtref_bar"))
-
-    def test_in_memory_db(self):
+        await i.unsafe_delete_table(table_name)
+        self.assertFalse(await i.has_table(table_name))
+
+        await i.unsafe_delete_tables()
+        self.assertFalse(await i.has_table(table_name))
+
+        await i.unsafe_delete_tables()
+        self.assertFalse(await i.has_table(table_name))
+
+    async def test_unsafe_delete_table_ref(self):
+        i, s1 = await self.create_table()
+        i, s2 = await self.create_table(
+            interface=i,
+            s1_id=Field(s1, True),
+            s1_2_id=Field(s1, True),
+        )
+
+        self.assertTrue(await i.has_table(s1))
+        await i.unsafe_delete_table(s1)
+        self.assertFalse(await i.has_table(s1))
+
+        await i.close()
+        self.assertFalse(i.is_connected())
+        self.assertTrue(await i.has_table(s2))
+        await i.unsafe_delete_tables()
+        self.assertFalse(await i.has_table(s2))
+
+    async def test_in_memory_db(self):
         i, s = self.get_table()
-        i.close()
         config = i.config
         config.database = ":memory:"
 
-        i.connect(config)
-        self.assertTrue(i.connected)
+        await i.connect(config)
+        self.assertTrue(i.is_connected())
 
-        _id = self.insert(i, s, 1)[0]
+        _id = (await self.insert(i, s, 1))
         self.assertTrue(_id)
 
+    async def test_get_fields_float(self):
+        """I'm not completely sure what this is testing anymore but I'm sure it
+        was a bug from some app that used ActiveRecord and I was trying to
+        read the sqlite db that app produced using prom. This doesn't work
+        as a postgres test because ZFLOAT isn't a valid field type"""
+        sql = "CREATE TABLE ZFOOBAR (ZFLOAT FLOAT)"
+
+        i = self.get_interface()
+        await i.raw(sql, ignore_result=True)
+        self.assertTrue(await i.has_table("ZFOOBAR"))
+
+        fields = await i.get_fields("ZFOOBAR")
+        self.assertEqual(float, fields["ZFLOAT"]["field_type"])
+
diff --git a/tests/model_test.py b/tests/model_test.py
index 39a1bd4..e516221 100644
--- a/tests/model_test.py
+++ b/tests/model_test.py
@@ -1,65 +1,33 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import pickle
 import json
 import datetime
 
 from datatypes import Datetime
 
-from . import BaseTestCase, EnvironTestCase, testdata
+from . import EnvironTestCase
 from prom.compat import *
-from prom.model import Orm, OrmPool
+from prom.model import Orm
 from prom.config import Field, Index
 from prom.query import Query
+from prom.exception import (
+    UniqueError,
+)
 import prom
 
 
-class OrmPoolTest(EnvironTestCase):
-    def test_lifecycle(self):
-        orm_class = self.get_orm_class()
-        pks = self.insert(orm_class, 10)
-
-        pool = OrmPool(orm_class, 1)
-
-        o = pool[pks[0]]
-        self.assertEqual(pks[0], o.pk)
-        o = pool[pks[0]]
-        self.assertEqual(pks[0], o.pk)
-        o = pool[pks[0]]
-        self.assertEqual(pks[0], o.pk)
-
-        pool[pks[1]]
-        self.assertEqual([2], list(pool.pq.keys()))
-
-        pool[pks[0]]
-        self.assertEqual([1], list(pool.pq.keys()))
-
-        pool[pks[1]]
-        self.assertEqual([2], list(pool.pq.keys()))
-
-        pool[pks[0]]
-        self.assertEqual([1], list(pool.pq.keys()))
-
-        pool = OrmPool(orm_class, len(pks) - 1)
-        for pk in pks:
-            o = pool[pk]
-            self.assertEqual(pk, o.pk)
-
-        self.assertEqual(list(pool.pq.keys())[0], pks[1])
-
-
 class OrmTest(EnvironTestCase):
-    def test_custom__id_pk(self):
+    async def test_custom__id_pk(self):
         orm_class = self.get_orm_class(
             _id=Field(str, True, max_size=36, pk=True)
         )
-        o = orm_class.create(_id="foo")
-        o2 = orm_class.query.one()
+        o = await orm_class.create(_id="foo")
+        o2 = await orm_class.query.one()
         self.assertEqual(o.pk, o2.pk)
 
     def test_overridden_field_aliases(self):
-        """Makes sure if we override a parent's defined Field instance the aliases
-        will still be set and point back to the overridden field"""
+        """Makes sure if we override a parent's defined Field instance the
+        aliases will still be set and point back to the overridden field"""
         orm_class = self.get_orm_class(_id=None, _created=None, _updated=None)
 
         o = orm_class()
@@ -90,21 +58,22 @@ def test_field_access(self):
             o.fkdasljfdkfsalk
 
     def test_aliases_1(self):
-        class Foo(Orm):
-            ip_address = Field(str, False, aliases=["ip"])
+        orm_class = self.get_orm_class(
+            ip_address=Field(str, False, aliases=["ip"])
+        )
 
         ip = "1.2.3.4"
 
-        f = Foo(ip="1.2.3.4")
+        f = orm_class(ip=ip)
         self.assertEqual(ip, f.ip)
         self.assertEqual(ip, f.ip_address)
 
-        f = Foo()
+        f = orm_class()
         f.ip = ip
         self.assertEqual(ip, f.ip)
         self.assertEqual(ip, f.ip_address)
 
-        f = Foo(ip="1.2.3.4")
+        f = orm_class(ip="1.2.3.4")
         del f.ip
         self.assertIsNone(f.ip)
         self.assertIsNone(f.ip_address)
@@ -117,30 +86,25 @@ def test_aliases_2(self):
         o = orm_class(foo2=1)
         self.assertEqual(1, o.foo)
 
-    def test_alias_pk(self):
+    async def test_alias_pk(self):
         orm_class = self.get_orm_class(model_name="o1")
-        o = self.insert_orm(orm_class)
+        o = await self.insert_orm(orm_class)
         self.assertEqual(o._id, o.o1_id)
         self.assertEqual(o._id, o.o1_pk)
         self.assertEqual(o._id, o.pk)
 
-    def test_removed_field(self):
+    async def test_removed_field(self):
+        """Assure removing a field doesn't cause querying to fail"""
         orm_class = self.get_orm_class()
-        o = orm_class.create(foo=1, bar="2")
-        o.save()
-
-        orm_class.schema.fields.pop("bar")
+        o = await orm_class.create(foo=1, bar="2")
+        await o.save()
+        self.assertLess(0, len(await orm_class.query.tolist()))
 
-        for count, o in enumerate(orm_class.query, 1):
-            pass
-        self.assertLess(0, count)
-
-    def test_required_field_not_set_update(self):
+    async def test_required_field_not_set_update(self):
         orm_class = self.get_orm_class()
 
         o = orm_class(foo=1, bar="two")
-
-        o.save()
+        await o.save()
 
         # create a new instance and make it look like an existing instance
         o2 = orm_class(
@@ -222,10 +186,10 @@ def test_to_interface_insert(self):
         fields = o.to_interface()
         self.assertFalse("_id" in fields)
 
-    def test_to_interface_update(self):
+    async def test_to_interface_update(self):
         orm_class = self.get_orm_class()
         o = orm_class(foo=1, bar="2")
-        o.save()
+        await o.save()
 
         fields = o.to_interface()
         self.assertEqual(1, len(fields)) # _updated would be the only field
@@ -239,7 +203,7 @@ def test_to_interface_update(self):
         with self.assertRaises(KeyError):
             fields = o.to_interface()
 
-    def test_created_updated(self):
+    async def test_created_updated(self):
         orm_class = self.get_orm_class()
 
         now = Datetime()
@@ -248,75 +212,77 @@ def test_created_updated(self):
         self.assertIsNone(o._created)
         self.assertIsNone(o._updated)
 
-        o.save()
+        await o.save()
         self.assertLess(now, o._created)
         self.assertLess(now, o._updated)
 
         _created = o._created
         _updated = o._updated
         o.foo=2
-        o.save()
+        await o.save()
         self.assertEqual(_created, o._created)
         self.assertLess(_updated, o._updated)
 
-    def test_created_set(self):
+    async def test_created_set(self):
+        tzinfo = datetime.timezone.utc
         orm_class = self.get_orm_class()
         o = orm_class(foo=1, bar="1")
 
-        _created = testdata.get_past_datetime().replace(tzinfo=datetime.timezone.utc)
+        _created = self.get_past_datetime().replace(tzinfo=tzinfo)
         o._created = _created
-        o.save()
+        await o.save()
 
-        o2 = o.query.eq_pk(o.pk).one()
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(_created, o2._created)
 
-        _created2 = testdata.get_past_datetime().replace(tzinfo=datetime.timezone.utc)
+        _created2 = self.get_past_datetime().replace(tzinfo=tzinfo)
         o2._created = _created2
-        o2.save()
+        await o2.save()
 
-        o3 = o.query.eq_pk(o.pk).one()
+        o3 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(_created2, o3._created)
 
-    def test_updated_set(self):
+    async def test_updated_set(self):
+        tzinfo = datetime.timezone.utc
         orm_class = self.get_orm_class()
         o = orm_class(foo=1, bar="1")
 
-        _updated = testdata.get_past_datetime().replace(tzinfo=datetime.timezone.utc)
+        _updated = self.get_past_datetime().replace(tzinfo=tzinfo)
         o._updated = _updated
-        o.save()
+        await o.save()
 
-        o2 = o.query.eq_pk(o.pk).one()
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(_updated, o2._updated)
 
-        _updated2 = testdata.get_past_datetime().replace(tzinfo=datetime.timezone.utc)
+        _updated2 = self.get_past_datetime().replace(tzinfo=tzinfo)
         o2._updated = _updated2
-        o2.save()
+        await o2.save()
 
-        o3 = o.query.eq_pk(o.pk).one()
+        o3 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(_updated2, o3._updated)
 
-    def test_hydrate_1(self):
-        """make sure you can add/update and change the primary key and all of that
-        works as expected"""
+    async def test_hydrate_1(self):
+        """make sure you can add/update and change the primary key and all of
+        that works as expected"""
         orm_class = self.get_orm_class()
 
         o = orm_class(foo=1, bar="1")
 
-        o.save()
+        await o.save()
         self.assertLess(0, o.pk)
 
-        with self.assertRaises(orm_class.interface.UniqueError):
+        with self.assertRaises(UniqueError):
             o2 = orm_class(_id=o.pk, foo=2, bar="2")
-            o2.save()
+            await o2.save()
 
-        o3 = o.query.one_pk(o.pk)
+        o3 = await o.query.eq_pk(o.pk).one()
         self.assertTrue(o3.is_hydrated())
 
         o3._id = o.pk + 1
         self.assertNotEqual(o.pk, o3.pk)
 
-        o3.save()
-        o4 = o3.query.one_pk(o3.pk)
+        await o3.save()
+        o4 = await o3.query.eq_pk(o3.pk).one()
         self.assertEqual(o3.pk, o4.pk)
         self.assertNotEqual(o.pk, o3.pk)
 
@@ -329,61 +295,61 @@ def test_hydrate_2(self):
         o = orm_class.hydrate(foo=1)
         self.assertEqual("lambda bar", o.bar)
 
-    def test_no_pk(self):
+    async def test_no_pk(self):
         orm_class = self.get_orm_class(_id=None)
 
-        pks = self.insert(orm_class, 1)
+        pks = await self.insert(orm_class, 1)
 
-        om1 = orm_class.query.one()
-        om2 = orm_class.query.is_foo(om1.foo).one()
+        om1 = await orm_class.query.one()
+        om2 = await orm_class.query.eq_foo(om1.foo).one()
         self.assertEqual(om1.foo, om2.foo)
 
-    def test_int_pk(self):
-        """Postgres was returning longs for primary keys in py2.7, this was different
-        behavior than SQLite and python 3, which returns int since 2.7+ transparently
-        handles ints of arbitrary size, this makes sure that ints are returned for
-        primary key"""
+    async def test_int_pk(self):
+        """Postgres was returning longs for primary keys in py2.7, this was
+        different behavior than SQLite and python 3, which returns int
+        since 2.7+ transparently handles ints of arbitrary size, this makes sure
+        that ints are returned for primary key"""
         orm_class = self.get_orm_class()
-        o = orm_class.create(foo=1, bar="1")
+        o = await orm_class.create(foo=1, bar="1")
         self.assertTrue(isinstance(o.pk, int))
 
-    def test_create_pk(self):
-        """there was a bug that if you set the pk then it wouldn't set the updated
-        or created datestamps, this makes sure that is fixed"""
+    async def test_create_pk(self):
+        """there was a bug that if you set the pk then it wouldn't set the
+        updated or created datestamps, this makes sure that is fixed"""
         orm_class = self.get_orm_class()
-        pk = testdata.get_int()
-        o = orm_class.create(foo=1, bar="1", _id=pk)
+        pk = self.get_int()
+        o = await orm_class.create(foo=1, bar="1", _id=pk)
         self.assertEqual(pk, o.pk)
 
-    def test_change_pk(self):
-
+    async def test_change_pk(self):
         # create a row at pk 1
         orm_class = self.get_orm_class()
-        o = orm_class.create(foo=1, bar="one")
+        o = await orm_class.create(foo=1, bar="one")
         self.assertEqual(1, o.pk)
 
         # move that row to pk 2
         o._id = 2
-        o.save()
+        await o.save()
 
-        o2 = o.query.one_pk(o.pk)
+        o2 = await o.query.eq_pk(o.pk).one()
 
         for k in o.schema.fields.keys():
             self.assertEqual(getattr(o, k), getattr(o2, k), k)
 
         o2.foo = 11
-        o2.save()
+        await o2.save()
 
         # now move it back to pk 1
         o2._id = 1
-        o2.save()
+        await o2.save()
 
-        o3 = o.query.one_pk(o2.pk)
+        o3 = await o.query.eq_pk(o2.pk).one()
         for k in o2.schema.fields.keys():
             self.assertEqual(getattr(o2, k), getattr(o3, k))
 
-        # we should only have 1 row in the db, our row we've changed from 1 -> 2 -> 1
-        self.assertEqual(1, o2.query.count())
+        # we should only have 1 row in the db, our row we've changed from
+        # 1 -> 2 -> 1
+        self.assertEqual(1, await o2.query.count())
 
     def test_overrides(self):
         orm_class = self.get_orm_class(_created=None)
@@ -392,21 +358,25 @@ def test_overrides(self):
         self.assertTrue("_updated" in s.fields)
         self.assertFalse("_created" in s.fields)
 
-    def test_field_iset(self):
-        """make sure a field with an iset method will be called at the correct time"""
+    async def test_field_iset_1(self):
+        """make sure a field with an iset method will be called at the correct
+        time"""
         orm_class = self.get_orm_class(
-            foo=Field(int, iset=lambda self, *_, **__: 100 if self.is_update() else 10)
+            foo=Field(
+                int,
+                iset=lambda self, *_, **__: 100 if self.is_update() else 10
+            )
         )
 
         o = orm_class()
-        o.insert()
+        await o.insert()
         self.assertEqual(10, o.foo)
 
         o.foo = 20
-        o.update()
+        await o.update()
         self.assertEqual(100, o.foo)
 
-    def test_field_iset_empty(self):
+    async def test_field_iset_empty(self):
         orm_class = self.get_orm_class(
             foo=Field(str, False, empty=False),
             bar=Field(int),
@@ -415,31 +385,33 @@ def test_field_iset_empty(self):
         # setting foo to empty triggers the empty check
         o = orm_class(foo="", bar=1)
         with self.assertRaises(ValueError):
-            o.insert()
+            await o.insert()
 
         # if foo isn't modified it doesn't trigger the empty check
         o = orm_class(bar=2)
-        pk = o.insert()
+        pk = await o.insert()
         self.assertLess(0, pk)
 
-    def test_field_iget(self):
-        """make sure a field with an iget method will be called at the correct time"""
+    async def test_field_iget(self):
+        """make sure a field with an iget method will be called at the correct
+        time"""
         orm_class = self.get_orm_class(
             foo=Field(int, iget=lambda *_, **__: 1000)
         )
         o = orm_class()
         o.foo = 20
-        o.insert()
+        await o.insert()
 
-        o2 = o.query.one_pk(o.pk)
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(1000, o2.foo)
 
-    def test_iget_iset_insert_update(self):
-        """Topher noticed when you set both iset and iget to wrap a value (in this case
-        to have a dict become a json string as it goes into the db but a dict any other
-        time) that the value would be iset correctly on insert/update, but then it wouldn't
-        go back to the iget value on success, this test makes sure that is fixed"""
-
+    async def test_iget_iset_insert_update(self):
+        """Topher noticed when you set both iset and iget to wrap a value (in
+        this case to have a dict become a json string as it goes into the db but
+        a dict any other time) that the value would be iset correctly on
+        insert/update, but then it wouldn't go back to the iget value on
+        success, this test makes sure that is fixed
+        """
         class IGetSetInsertUpdateOrm(Orm):
             table_name = self.get_table_name()
             interface = self.get_interface()
@@ -458,47 +430,31 @@ def foo(cls, val):
         o = IGetSetInsertUpdateOrm()
         o.foo = {"foo": 1, "bar": 2}
         self.assertTrue(isinstance(o.foo, dict))
-        o.insert()
+        await o.insert()
         self.assertTrue(isinstance(o.foo, dict))
 
         o.foo = {"foo": 2, "bar": 1}
         self.assertTrue(isinstance(o.foo, dict))
-        o.update()
+        await o.update()
         self.assertTrue(isinstance(o.foo, dict))
 
-        o2 = o.query.one_pk(o.pk)
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertTrue(isinstance(o2.foo, dict))
         self.assertEqual(o.foo, o2.foo)
 
-    def test_field_getattr(self):
-        class FOFieldGAOrm(Orm):
-            table_name = "fofgaorm_table"
-            foo = Field(int)
-            @foo.fsetter
-            def foo(self, val):
-                return getattr(self, "bar", 10)
-
-            bar = Field(int)
-            @bar.fsetter
-            def bar(self, val):
-                return getattr(self, "foo", 10)
-
-        # this test passes if it doesn't raise an exception
-        o = FOFieldGAOrm()
-
-    def test_field_lifecycle(self):
+    async def test_field_lifecycle(self):
         orm_class = self.get_orm_class(
             foo=Field(int)
         )
 
-        o = orm_class.create(foo=1)
+        o = await orm_class.create(foo=1)
         self.assertEqual(1, o.foo)
 
         o.foo = 2
         self.assertTrue("foo" in o.modified_fields)
 
-        o.save()
-        o2 = o.query.one_pk(o.pk)
+        await o.save()
+        o2 = await o.query.eq_pk(o.pk).one()
         self.assertEqual(2, o.foo)
 
         del o.foo
@@ -530,7 +486,7 @@ def test___delattr__(self):
         with self.assertRaises(AttributeError):
             o.che
 
-    def test___setattr__(self):
+    async def test___setattr__(self):
         orm_class = self.get_orm_class(
             foo=Field(int),
             bar=Field(str)
@@ -541,58 +497,58 @@ def test___setattr__(self):
         o.bar = "1"
         self.assertTrue(o.modified_fields)
 
-        o.save()
+        await o.save()
         self.assertFalse(o.modified_fields)
 
         o.foo = 2
         self.assertTrue(o.modified_fields)
 
-        o.save()
+        await o.save()
         self.assertFalse(o.modified_fields)
 
         o.foo = None
         o.bar = None
         self.assertEqual(2, len(o.modified_fields))
 
-    def test___getattr___orm_1(self):
+    async def test___getattr___orm_1(self):
         foo_class = self.get_orm_class(model_name="foo")
         bar_class = self.get_orm_class(foo_id=Field(foo_class, True))
 
-        foo_id = self.insert_fields(foo_class)
+        foo_id = await self.insert_fields(foo_class)
         bar = bar_class(foo_id=foo_id)
 
-        foo = bar.foo
+        foo = await bar.foo
         self.assertEqual(foo_id, foo.pk)
 
-        foo2_id = self.insert_fields(foo_class)
-        foo2 = foo_class.query.eq_pk(foo2_id).one()
+        foo2_id = await self.insert_fields(foo_class)
+        foo2 = await foo_class.query.eq_pk(foo2_id).one()
 
         bar.foo = foo2
-        bar.save()
+        await bar.save()
         self.assertEqual(foo2.pk, bar.foo_id)
 
-        foo2 = bar.foo
+        foo2 = await bar.foo
         self.assertEqual(foo2_id, foo2.pk)
         self.assertNotEqual(foo_id, foo2_id)
 
-    def test___getattr___orm_2(self):
+    async def test___getattr___orm_2(self):
         o1_class = self.get_orm_class(
             bar=Field(bool),
             che=Field(str),
             model_name="o1",
         )
         o2_class = self.get_orm_class(o1_id=Field(o1_class))
-        o1 = self.insert_orm(o1_class, bar=False, che="1")
+        o1 = await self.insert_orm(o1_class, bar=False, che="1")
         self.assertLess(0, o1.pk)
         self.assertFalse(o1.bar)
 
-        o2 = self.insert_orm(o2_class, o1_id=o1.pk)
-        r1 = o2.o1
+        o2 = await self.insert_orm(o2_class, o1_id=o1.pk)
+        r1 = await o2.o1
         self.assertEqual(o1.pk, r1.pk)
         self.assertEqual(o1.bar, r1.bar)
         self.assertEqual(o1.che, r1.che)
 
-    def test___getattr___orm_3(self):
+    async def test___getattr___orm_3(self):
         """Makes sure referencing a foreign key field by the model_name returns
         None. This was originally in extras.MagicOrmTest"""
         o1_class = self.get_orm_class(
@@ -602,39 +558,40 @@ def test___getattr___orm_3(self):
         )
         o2_class = self.get_orm_class(o1_id=Field(o1_class))
 
-        o1 = self.insert_orm(o1_class)
-        o2 = self.insert_orm(o2_class)
+        o1 = await self.insert_orm(o1_class)
+        o2 = await self.insert_orm(o2_class)
 
-        self.assertIsNone(o2.o1)
+        self.assertIsNone(await o2.o1)
         with self.assertRaises(AttributeError):
             o2.blahblah
 
-    def test___getattr___models_name(self):
-        """Makes sure that getting an attribute by models_name works as expected"""
+    async def test___getattr___models_name(self):
+        """Makes sure that getting an attribute by models_name works as expected
+        """
         o1_class = self.get_orm_class(
             bar=Field(str),
             model_name="o1",
-            #models_name="o1s",
         )
         o2_class = self.get_orm_class(
             o1_id=Field(o1_class),
             model_name="o2",
         )
 
-        o1 = self.insert_orm(o1_class, bar="1")
-        self.insert_orm(o1_class, bar="ignored")
+        o1 = await self.insert_orm(o1_class, bar="1")
+        await self.insert_orm(o1_class, bar="ignored")
 
-        o2 = self.insert_orm(o2_class, o1_id=o1.pk)
-        o2 = self.insert_orm(o2_class, o1_id=o1.pk)
+        o2 = await self.insert_orm(o2_class, o1_id=o1.pk)
+        o2 = await self.insert_orm(o2_class, o1_id=o1.pk)
 
         count = 0
-        for o2r in o1.o2s:
+        async for o2r in await o1.o2s:
             count += 1
             self.assertEqual(o1.pk, o2.o1_id)
         self.assertEqual(2, count)
 
-    def test___getattr___model_name(self):
-        """Makes sure that getting an attribute by model_name works as expected"""
+    async def test___getattr___model_name(self):
+        """Makes sure that getting an attribute by model_name works as expected
+        """
         o1_class = self.get_orm_class(
             bar=Field(str),
             model_name="o1model",
@@ -644,25 +601,26 @@ def test___getattr___model_name(self):
             model_name="o2model",
         )
 
-        o1 = self.insert_orm(o1_class, bar="1")
-        o2 = self.insert_orm(o2_class, o1_id=o1.pk)
+        o1 = await self.insert_orm(o1_class, bar="1")
+        o2 = await self.insert_orm(o2_class, o1_id=o1.pk)
 
-        o2r = o1.o2model
+        o2r = await o1.o2model
         self.assertEqual(o2.pk, o2r.pk)
 
     def test_creation(self):
-        class COrm(Orm):
-            foo = Field(int)
+        orm_class = self.get_orm_class(
+            foo = Field(int),
             bar = Field(str)
+        )
 
-        s = COrm.schema
+        s = orm_class.schema
         self.assertTrue(s.foo)
         self.assertTrue(s.bar)
         self.assertTrue(s.pk)
         self.assertTrue(s._created)
         self.assertTrue(s._updated)
 
-    def test_none(self):
+    async def test_none(self):
         orm_class = self.get_orm_class()
         orm_class.foo.required = False
         orm_class.bar.required = False
@@ -671,11 +629,11 @@ def test_none(self):
         t2 = orm_class(foo=None, bar=None)
         self.assertEqual(t1.fields, t2.fields)
 
-        t1.save()
-        t2.save()
+        await t1.save()
+        await t2.save()
 
-        t11 = orm_class.query.one_pk(t1.pk)
-        t22 = orm_class.query.one_pk(t2.pk)
+        t11 = await orm_class.query.eq_pk(t1.pk).one()
+        t22 = await orm_class.query.eq_pk(t2.pk).one()
         ff = lambda orm: orm.schema.normal_fields
         self.assertEqual(ff(t11), ff(t22))
         self.assertEqual(ff(t1), ff(t11))
@@ -684,14 +642,14 @@ def test_none(self):
         t3 = orm_class(foo=1)
         self.assertEqual(1, t3.foo)
         self.assertEqual(None, t3.bar)
-        t3.save()
+        await t3.save()
         self.assertEqual(1, t3.foo)
         self.assertEqual(None, t3.bar)
-        t3 = orm_class.query.one_pk(t3.pk)
+        t3 = await orm_class.query.eq_pk(t3.pk).one()
         self.assertEqual(1, t3.foo)
         self.assertEqual(None, t3.bar)
 
-    def test_jsonable(self):
+    def test_jsonable_1(self):
         orm_class = self.get_orm_class()
         orm_class.dt = Field(datetime.datetime)
         t = orm_class.hydrate(foo=1, bar="blah", dt=datetime.datetime.utcnow())
@@ -750,7 +708,7 @@ def che(self, field_val):
         self.assertIsNone(t._created)
         self.assertIsNone(t._updated)
 
-    def test_modify_2(self):
+    async def test_modify_2(self):
         orm_class = self.get_orm_class(
             foo=Field(str, False),
             bar=Field(dict, False, default=dict),
@@ -763,35 +721,35 @@ def test_modify_2(self):
             self.assertTrue(k in t.modified_fields)
 
         t.bar["foo"] = 1
-        t.save()
+        await t.save()
 
-        t2 = t.query.one_pk(t.pk)
+        t2 = await t.query.eq_pk(t.pk).one()
         self.assertEqual(t.bar["foo"], t2.bar["foo"])
 
         t2.bar["foo"] = 2
-        t2.save()
+        await t2.save()
 
-        t3 = t.query.one_pk(t.pk)
+        t3 = await t.query.eq_pk(t.pk).one()
         self.assertEqual(t3.bar["foo"], t2.bar["foo"])
 
-    def test_modify_none(self):
+    async def test_modify_none(self):
         orm_class = self.get_orm_class(
             foo=Field(str, False)
         )
 
         o = orm_class()
         o.foo = 1
-        o.save()
+        await o.save()
 
-        o2 = o.query.one_pk(o.pk)
+        o2 = await o.query.eq_pk(o.pk).one()
         o2.foo = None
-        o2.save()
+        await o2.save()
         self.assertIsNone(o2.foo)
 
-        o3 = o.query.one_pk(o.pk)
+        o3 = await o.query.eq_pk(o.pk).one()
         self.assertIsNone(o3.foo)
 
-    def test_modified_1(self):
+    async def test_modified_1(self):
         orm_class = self.get_orm_class()
         o = orm_class(foo=1, bar="2")
 
@@ -800,7 +758,7 @@ def test_modified_1(self):
         for field_name in ["foo", "bar"]:
             self.assertTrue(field_name in mfs)
 
-        o.save()
+        await o.save()
 
         mfs = o.modified_fields
         self.assertEqual(0, len(mfs))
@@ -810,53 +768,26 @@ def test_modified_1(self):
         self.assertEqual(1, len(mfs))
         self.assertTrue("foo" in mfs)
 
-        o2 = o.requery()
+        o2 = await o.requery()
         mfs = o2.modified_fields
         self.assertEqual(0, len(mfs))
 
-    def test_unicode(self):
-        """
-        Jarid was having encoding issues, so I'm finally making sure prom only ever
-        returns unicode strings
-        """
-        orm_class = self.get_orm_class(
-            foo=Field(unicode, True),
-            bar=Field(str, True),
-            che=Field(str, False),
-            baz=Field(int, False),
-        )
-
-        t = orm_class.create(
-            foo=testdata.get_unicode_name(),
-            bar=testdata.get_unicode_words(),
-            che=testdata.get_unicode_words().encode('utf-8'),
-            baz=testdata.get_int(1, 100000)
-        )
-
-        t2 = orm_class.query.one_pk(t.pk)
-
-        self.assertEqual(t.foo, t2.foo)
-        self.assertEqual(t.bar, t2.bar)
-        #self.assertEqual(t.che, t2.che.encode('utf-8'))
-
-        self.assertEqual(t.che.decode("utf-8"), t2.che)
-        self.assertTrue(isinstance(t.baz, int))
-
-    def test_query(self):
+    async def test_query(self):
         orm_class = self.get_orm_class()
-        pks = self.insert(orm_class, 5)
-        lc = orm_class.query.in_pk(pks).count()
+        pks = await self.insert(orm_class, 5)
+        lc = await orm_class.query.in_pk(pks).count()
         self.assertEqual(len(pks), lc)
 
-    def test___int__(self):
+    async def test___int__(self):
         orm_class = self.get_orm_class()
-        pk = self.insert(orm_class, 1)[0]
-        t = orm_class.query.one_pk(pk)
+        pk = await self.insert(orm_class, 1)
+        t = await orm_class.query.eq_pk(pk).one()
         self.assertEqual(pk, int(t))
 
     def test_query_class(self):
-        """make sure you can set the query class and it is picked up correctly"""
-        class QueryClassTormQuery(prom.Query):
+        """make sure you can set the query class and it is picked up correctly
+        """
+        class QueryClassTormQuery(Query):
             pass
 
         class QueryClassTorm(Orm):
@@ -865,10 +796,10 @@ class QueryClassTorm(Orm):
 
         other_orm_class = self.get_orm_class()
         self.assertEqual(QueryClassTorm.query_class, QueryClassTormQuery)
-        self.assertEqual(other_orm_class.query_class, prom.Query)
+        self.assertEqual(other_orm_class.query_class, Query)
 
     def test_property_autodiscover(self):
-        m = testdata.create_module([
+        m = self.create_module([
             "import prom",
             "",
             "class FooQuery(prom.Query):",
@@ -897,7 +828,10 @@ def test_property_autodiscover(self):
         self.assertEqual(f.query_class, fooq.Foo.query_class)
 
         f = fooq.Foo()
-        self.assertEqual(f.query.__class__.__name__, fooq.Foo.query.__class__.__name__)
+        self.assertEqual(
+            f.query.__class__.__name__,
+            fooq.Foo.query.__class__.__name__
+        )
 
         f = fooq.Foo()
         #self.assertEqual(f.interface, fooq.Foo.interface)
@@ -907,7 +841,10 @@ def test_property_autodiscover(self):
         self.assertEqual(fooq.Bar.query_class, b.query_class)
 
         b = fooq.Bar()
-        self.assertEqual(fooq.Bar.query.__class__.__name__, b.query.__class__.__name__)
+        self.assertEqual(
+            fooq.Bar.query.__class__.__name__,
+            b.query.__class__.__name__
+        )
 
         b = fooq.Bar()
         #self.assertEqual(fooq.Bar.interface, b.interface)
@@ -929,7 +866,8 @@ def test_interface(self):
         i = orm_class.interface
         self.assertFalse(i is None)
 
-        # now let's make sure a different orm with a bad connection name gets flagged
+        # now let's make sure a different orm with a bad connection name gets
+        # flagged
         orm_class = self.get_orm_class(
             connection_name="blkasdfjksdafjdkfklsd"
         )
@@ -937,7 +875,7 @@ def test_interface(self):
         with self.assertRaises(KeyError):
             i = orm_class.interface
 
-    def test___init__(self):
+    def test___init___1(self):
         orm_class = self.get_orm_class()
         t = orm_class(foo=1)
         self.assertTrue('foo' in t.modified_fields)
@@ -975,66 +913,67 @@ def test___init___default_fset(self):
         self.assertEqual(2, o.bar)
         self.assertEqual(3, o.che)
 
-    def test_save(self):
+    async def test_save(self):
         orm_class = self.get_orm_class()
         t = orm_class()
         with self.assertRaises(KeyError):
-            t.save()
+            await t.save()
 
         t = orm_class(foo=1, bar="value 1", this_is_ignored="as it should be")
         self.assertEqual(1, t.foo)
         self.assertEqual("value 1", t.bar)
         self.assertIsNone(t.pk)
         self.assertTrue(t.is_modified())
-        t.save()
+        await t.save()
         self.assertIsNotNone(t.pk)
         self.assertFalse(t.is_modified())
 
         t.foo = 2
         t.bar = "value 2"
         self.assertTrue(t.is_modified())
-        t.save()
+        await t.save()
         self.assertEqual(2, t.foo)
         self.assertEqual("value 2", t.bar)
 
-        # set should only update timestamps and stuff without changing unmodified values
+        # set should only update timestamps and stuff without changing
+        # unmodified values
         self.assertFalse(t.is_modified())
-        r = t.save()
+        r = await t.save()
         self.assertTrue(r)
 
         # make sure it persisted
-        t.interface.close()
-        t2 = orm_class.query.is_pk(t.pk).one()
+        await t.interface.close()
+        t2 = await orm_class.query.eq_pk(t.pk).one()
         self.assertFalse(t2.is_modified())
         self.assertEqual(2, t2.foo)
         self.assertEqual("value 2", t2.bar)
         self.assertEqual(t.fields, t2.fields)
 
-    def test_delete(self):
+    async def test_delete(self):
         t = self.get_orm(foo=1, bar="value 1")
-        r = t.delete()
+        r = await t.delete()
         self.assertFalse(r)
-        t.save()
+        await t.save()
         self.assertTrue(t.pk)
         _id = t.pk
 
-        t.delete()
+        await t.delete()
         self.assertFalse(t.pk)
         self.assertTrue(t.is_modified())
 
         # make sure it persists
-        t.interface.close()
-        t2 = t.query.one_pk(_id)
+        await t.interface.close()
+        t2 = await t.query.eq_pk(_id).one()
         self.assertEqual(None, t2)
 
-    def test_create_1(self):
+    async def test_create_1(self):
         orm_class = self.get_orm_class()
-        t = orm_class.create(foo=1000, bar="value1000")
+        t = await orm_class.create(foo=1000, bar="value1000")
         self.assertLess(0, t.pk)
         self.assertEqual(1000, t.foo)
         self.assertEqual("value1000", t.bar)
 
-    def test_create_2(self):
+    async def test_create_2(self):
         """https://github.com/Jaymon/prom/issues/124"""
         kwargs = {
             "foo": 1,
@@ -1042,16 +981,16 @@ def test_create_2(self):
         }
         orm_class = self.get_orm_class()
 
-        o = orm_class.create(**kwargs)
+        o = await orm_class.create(**kwargs)
         self.assertTrue(isinstance(o._created, datetime.datetime))
 
         kwargs["_id"] = o.pk + 1
-        o2 = orm_class.create(**kwargs)
+        o2 = await orm_class.create(**kwargs)
         self.assertTrue(isinstance(o._created, datetime.datetime))
 
-    def test_fields(self):
+    async def test_fields(self):
         orm_class = self.get_orm_class()
-        t = orm_class.create(foo=1000, bar="value1000")
+        t = await orm_class.create(foo=1000, bar="value1000")
         d = t.fields
         for f in t.schema.fields:
             self.assertTrue(f in d)
@@ -1060,8 +999,8 @@ def test_fields(self):
         d["_id"] = d["_id"] + 1
         self.assertNotEqual(d["_id"], t.pk)
 
-    def test_pickling(self):
-        mpath = testdata.create_module([
+    async def test_pickling(self):
+        mpath = self.create_module([
             "from prom import Orm, Field, Index",
             "",
             "class PickleOrm(Orm):",
@@ -1079,19 +1018,19 @@ def test_pickling(self):
         self.assertEqual(t.fields, t2.fields)
         self.assertEqual(t.modified_fields, t2.modified_fields)
 
-        t.save()
+        await t.save()
         p = pickle.dumps(t)
         t2 = pickle.loads(p)
         self.assertEqual(t.fields, t2.fields)
         self.assertEqual(t.modified_fields, t2.modified_fields)
 
         t2.foo += 1
-        t2.save()
+        await t2.save()
 
-        t3 = PickleOrm.query.one_pk(t2.pk)
+        t3 = await PickleOrm.query.eq_pk(t2.pk).one()
         self.assertEqual(t3.fields, t2.fields)
 
-    def test_transaction(self):
+    async def test_transaction(self):
         """with transaction context managers weren't working correctly when the
         second insert would fail, the first insert was still going through, this
         test helped me reproduce, diagnose, and fix the problem"""
@@ -1101,13 +1040,13 @@ class TransTorm1(Orm):
             foo = Field(str, True)
 
             @classmethod
-            def creation(cls, d):
-                with cls.interface.transaction():
+            async def creation(cls, d):
+                async with cls.interface.transaction():
                     d['foo'] = "foo"
-                    tt = cls.create(**d)
+                    tt = await cls.create(**d)
 
                     d['tt1_id'] = tt.pk
-                    m = TransTorm2.create(**d)
+                    m = await TransTorm2.create(**d)
 
                 return tt
 
@@ -1116,22 +1055,20 @@ class TransTorm2(Orm):
             bar = Field(str, True, max_size=10)
             tt1_id = Field(TransTorm1, True)
 
-        TransTorm1.install()
-        TransTorm2.install()
+        await TransTorm1.install()
+        await TransTorm2.install()
 
         # actual test starts here
 
-        self.assertEqual(0, TransTorm1.query.count())
+        self.assertEqual(0, await TransTorm1.query.count())
 
-        #d = {"bar": testdata.get_ascii(32)}
         d = {}
-        #with self.assertRaises(prom.InterfaceError):
         with self.assertRaises(Exception):
-            tt = TransTorm1.creation(d)
+            tt = await TransTorm1.creation(d)
 
-        self.assertEqual(0, TransTorm1.query.count())
+        self.assertEqual(0, await TransTorm1.query.count())
 
-    def test_non_int_primary_key(self):
+    async def test_non_int_primary_key(self):
         Nipk = self.get_orm_class(
             _id=Field(str, True, pk=True, max_size=64)
         )
@@ -1141,9 +1078,9 @@ def test_non_int_primary_key(self):
 
         # since our pk no longer is auto-increment we always have to provide it
         with self.assertRaises(prom.InterfaceError):
-            Nipk.create()
+            await Nipk.create()
 
-        n = Nipk.create(_id="pk1")
+        n = await Nipk.create(_id="pk1")
         self.assertEqual("pk1", n.pk)
         self.assertEqual("pk1", n._id)
 
@@ -1152,29 +1089,30 @@ def test_non_int_primary_key(self):
         self.assertEqual("pk1", str(n))
 
         with self.assertRaises(prom.InterfaceError):
-            Nipk.create(_id="pk1")
+            await Nipk.create(_id="pk1")
 
-        n2 = Nipk2.create(nipk_id=n.pk)
+        n2 = await Nipk2.create(nipk_id=n.pk)
         self.assertEqual(n.pk, n2.nipk_id)
 
-    def test_failure_save(self):
-        """test to make sure saving on a table that doesn't exist doesn't actually fail"""
+    async def test_failure_save(self):
+        """test to make sure saving on a table that doesn't exist doesn't
+        actually fail"""
         orm_class = self.get_orm_class()
         f = orm_class(foo=1, bar="value 1")
-        f.save()
+        await f.save()
         self.assertTrue(f.pk)
 
-    def test_failure_get(self):
-        """test to make sure getting on a table that doesn't exist works without raising
-        an error
+    async def test_failure_get(self):
+        """test to make sure getting on a table that doesn't exist works without
+        raising an error
         """
         orm_class = self.get_orm_class()
         o = orm_class(foo=1, bar="value 1")
-        o2 = o.query.one()
+        o2 = await o.query.one()
         # we succeeded if no error was raised
 
     def test_fk(self):
-        mpath = testdata.create_module([
+        mpath = self.create_module([
             "from prom import Field, Orm",
             "",
             "class Foo(Orm):",
@@ -1209,28 +1147,29 @@ def test_fk(self):
         with self.assertRaises(ValueError):
             c.fk(Boo)
 
-    def test_subquery_1(self):
+    async def test_subquery_1(self):
         count = 10
         Foo = self.get_orm_class()
         Bar = self.get_orm_class(
             foo_id=Field(Foo, True),
         )
 
-        foo_ids = self.insert(Foo, count)
+        foo_ids = await self.insert(Foo, count)
         for foo_id in foo_ids:
-            Bar.create(foo_id=foo_id)
+            await Bar.create(foo_id=foo_id)
 
         q = Bar.query.in_foo_id(Foo.query.select_pk())
-        self.assertEqual(count, len(q.get()))
+        self.assertEqual(count, len(await q.tolist()))
 
         q = Bar.query.in_foo_id(Foo.query.select_pk().gt_pk(count + 10000))
-        self.assertEqual(0, len(q.get()))
+        self.assertEqual(0, len(await q.tolist()))
 
-        q = Bar.query.is_foo_id(Foo.query.select_pk().limit(1))
-        self.assertEqual(1, len(q.get()))
+        q = Bar.query.eq_foo_id(Foo.query.select_pk().limit(1))
+        self.assertEqual(1, len(await q.tolist()))
 
-    def test_subquery_2(self):
-        """Similar test as subquery_1 but makes sure query_class works as expected also"""
+    async def test_subquery_2(self):
+        """Similar test as subquery_1 but makes sure query_class works as
+        expected also"""
         count = 10
         class BarQuery(Query): pass
         Foo = self.get_orm_class()
@@ -1239,14 +1178,14 @@ class BarQuery(Query): pass
             query_class=BarQuery,
         )
 
-        foo_ids = self.insert(Foo, count)
+        foo_ids = await self.insert(Foo, count)
         for foo_id in foo_ids:
-            Bar.create(foo_id=foo_id)
+            await Bar.create(foo_id=foo_id)
 
         q = Bar.query.in_foo_id(Foo.query.select_pk())
-        self.assertEqual(count, len(q.get()))
+        self.assertEqual(count, len(await q.tolist()))
 
-    def test_upsert_1(self):
+    async def test_upsert_1(self):
         orm_class = self.get_orm_class(
             foo=Field(str, True),
             bar=Field(str, True),
@@ -1256,18 +1195,18 @@ def test_upsert_1(self):
         )
 
         o = orm_class({"foo": "1", "bar": "1", "che": "1", "baz": 1})
-        o.upsert()
-        o2 = orm_class.query.eq_pk(o.pk).one()
+        await o.upsert()
+        o2 = await orm_class.query.eq_pk(o.pk).one()
         self.assertEqual(o.baz, o2.baz)
         self.assertEqual(o.pk, o2.pk)
 
         o3 = orm_class({"foo": "1", "bar": "1", "che": "1", "baz": 2})
-        o3.upsert()
+        await o3.upsert()
         self.assertEqual(o.pk, o3.pk)
-        o4 = orm_class.query.eq_pk(o3.pk).one()
+        o4 = await orm_class.query.eq_pk(o3.pk).one()
         self.assertEqual(o3.baz, o4.baz)
 
-    def test_upsert_nochange(self):
+    async def test_upsert_nochange(self):
         orm_class = self.get_orm_class(
             foo=Field(int, True),
             bar=Field(int, True),
@@ -1275,12 +1214,12 @@ def test_upsert_nochange(self):
         )
 
         o = orm_class(foo=1, bar=1)
-        r1 = o.upsert()
+        r1 = await o.upsert()
         self.assertTrue(r1)
-        r2 = o.upsert()
+        r2 = await o.upsert()
         self.assertTrue(r2)
 
-    def test_upsert_with_pk(self):
+    async def test_upsert_with_pk(self):
         orm_class = self.get_orm_class(
             foo=Field(int, True),
             bar=Field(int, True),
@@ -1288,14 +1227,14 @@ def test_upsert_with_pk(self):
             foo_bar=Index("foo", "bar", unique=True),
         )
 
-        o = orm_class.create(foo=1, bar=2)
+        o = await orm_class.create(foo=1, bar=2)
 
         o.baz = 3
-        o.upsert()
-        o2 = o.requery()
+        await o.upsert()
+        o2 = await o.requery()
         self.assertEqual(o.baz, o2.baz)
 
-    def test_load(self):
+    async def test_load(self):
         orm_class = self.get_orm_class(
             foo=Field(int, True),
             bar=Field(int, True),
@@ -1307,27 +1246,27 @@ def test_load(self):
 
         o = orm_class(foo=1, che=1, baz=1)
 
-        self.assertFalse(o.load())
+        self.assertFalse(await o.load())
 
         o.bar = 1
-        o.save()
+        await o.save()
 
         o2 = orm_class(che=1, baz=1)
-        self.assertTrue(o2.load())
+        self.assertTrue(await o2.load())
         self.assertEqual(o.pk, o2.pk)
         for field_name, field_value in o.fields.items():
             self.assertEqual(field_value, getattr(o2, field_name), field_name)
 
-        o2.save()
+        await o2.save()
         for field_name, field_value in o.fields.items():
             self.assertEqual(field_value, getattr(o2, field_name), field_name)
 
         o3 = orm_class(pk=o2.pk)
-        o3.load()
+        await o3.load()
         for field_name, field_value in o.fields.items():
             self.assertEqual(field_value, getattr(o3, field_name), field_name)
 
-    def test_new_fields_read(self):
+    async def test_new_fields_read(self):
         """Makes sure a new field added to the orm is seemlessly handled on a
         select query
 
@@ -1336,11 +1275,11 @@ def test_new_fields_read(self):
         a raw value), but prom should still handle it correctly
         """
         orm_class = self.get_orm_class()
-        pks = self.insert(orm_class, 1)
+        pk = await self.insert(orm_class, 1)
 
         orm_class.che = Field(str, False)
         orm_class.schema.set_field("che", orm_class.che)
 
-        o = orm_class.query.eq_pk(pks[0]).one()
+        o = await orm_class.query.eq_pk(pk).one()
         self.assertIsNone(o.che)
 
diff --git a/tests/query_test.py b/tests/query_test.py
index d27ddba..571cba6 100644
--- a/tests/query_test.py
+++ b/tests/query_test.py
@@ -1,14 +1,13 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 import datetime
-import time
-from threading import Thread
-import sys
 import re
+import inspect
+import random
 
-import testdata
-
-from . import BaseTestCase, EnvironTestCase, TestCase, SkipTest
+from . import (
+    EnvironTestCase,
+    TestCase,
+)
 from prom.query import (
     Query,
     QueryBounds,
@@ -16,21 +15,19 @@
     QueryFields,
     Iterator,
 )
-from prom.config import Field as OrmField
+from prom.config import Field
 from prom.compat import *
-import prom
 
 
-class QueryFieldTest(BaseTestCase):
+class QueryFieldTest(TestCase):
     def test___new__(self):
         q = self.get_query()
         f = QueryField(q, "MAX(foo)")
-        #f = QueryField("MAX(foo)", schema=testdata.mock(field_name="foo"))
         self.assertEqual("foo", f.name)
         self.assertEqual("MAX", f.function_name)
 
 
-class QueryFieldsTest(BaseTestCase):
+class QueryFieldsTest(TestCase):
     def test_fields(self):
         q = self.get_query()
         fs = QueryFields()
@@ -149,25 +146,231 @@ def test_paginate_limit(self):
 
 
 class QueryTest(EnvironTestCase):
-    def test_query_syntactic_sugar(self):
-        Foo = self.get_orm_class()
-        self.insert(Foo, 5)
+    async def test_cursor(self):
+        q = self.get_query()
+        cursor = await q.cursor()
+        self.assertFalse(inspect.iscoroutine(cursor))
+        self.assertIsNotNone(cursor)
+
+    async def test_get_1(self):
+        orm_class = self.get_orm_class()
+        _ids = await self.insert(orm_class, 2)
+
+        count = 0
+        async for o in await orm_class.query.get():
+            count += 1
+            self.assertEqual(orm_class, type(o))
+            self.assertTrue(o._id in _ids)
+            self.assertFalse(o.is_modified())
+        self.assertEqual(count, len(_ids))
+
+    async def test_get_all(self):
+        count = 10
+        q = self.get_query()
+        await self.insert(q, count)
+
+        # if no limit is set then it should go through all results
+        rcount = 0
+        async for r in await q.copy().get():
+            rcount += 1
+        self.assertEqual(count, rcount)
+
+        # if there is a limit then all should only go until that limit
+        rcount = 0
+        async for r in await q.copy().limit(1).get():
+            rcount += 1
+        self.assertEqual(1, rcount)
+
+        # only go until the end of the results
+        rcount = 0
+        async for r in await q.copy().limit(6).offset(6).get():
+            rcount += 1
+        self.assertEqual(4, rcount)
+
+    async def test_get_pk(self):
+        orm_class = self.get_orm_class()
+        pks = await self.insert(orm_class, 2)
+
+        res = await orm_class.query.select_pk().in_pk(pks).get()
+        rpks = [r async for r in res]
+        self.assertEqual(2, len(rpks))
+        self.assertEqual(pks, rpks)
+
+    async def test_get_values(self):
+        q = self.get_query()
+
+        count = 2
+        pks = await self.insert(q, count)
+
+        vals = [r async for r in q.copy().select_foo()]
+        self.assertEqual(count, len(vals))
+        for v in vals:
+            self.assertTrue(isinstance(v, int))
+
+        vals = [r async for r in q.copy().select_foo().select_bar()]
+        self.assertEqual(count, len(vals))
+        for v in vals:
+            self.assertTrue(isinstance(v, list))
+
+        vals = [r async for r in q.copy().select_foo().limit(1)]
+        self.assertEqual(1, len(vals))
+
+    async def test_one_1(self):
+        orm_class = self.get_orm_class()
+        _ids = await self.insert(orm_class, 2)
+
+        o = await orm_class.query.one()
+        self.assertEqual(orm_class, type(o))
+        self.assertTrue(o._id in _ids)
+        self.assertFalse(o.is_modified())
+
+    async def test_one_value(self):
+        _q = self.get_query()
+
+        v = await _q.copy().select_foo().one()
+        self.assertEqual(None, v)
+
+        count = 2
+        pks = await self.insert(_q, count)
+
+        o = await _q.copy().eq_pk(pks[0]).one()
+        v = await _q.copy().select_foo().eq_pk(o.pk).one()
+        self.assertEqual(o.foo, v)
+
+        v = await _q.copy().select_foo().select_bar().eq_pk(o.pk).one()
+        self.assertEqual(o.foo, v[0])
+        self.assertEqual(o.bar, v[1])
+
+    async def test_count(self):
+        orm_class = self.get_orm_class()
+        await self.insert(orm_class, 10)
+
+        self.assertEqual(5, await orm_class.query.offset(5).count())
+        self.assertEqual(5, await orm_class.query.limit(5).count())
+        self.assertEqual(10, await orm_class.query.count())
+
+    async def test_has(self):
+        q = self.get_query()
+        self.assertFalse(await q.has())
+
+        await self.insert(q, 1)
+        self.assertTrue(await q.has())
+
+    async def test_insert_and_update(self):
+        orm_class = self.get_orm_class(
+            foo=Field(int),
+            bar=Field(str),
+            _created=None,
+            _updated=None
+        )
+        fields = dict(foo=1, bar="value 1")
+
+        pk = await orm_class.query.set(fields).insert()
+        self.assertLess(0, pk)
+
+        o = await orm_class.query.eq_pk(pk).one()
+        self.assertEqual(pk, o.pk)
+        self.assertEqual(1, o.foo)
+        self.assertTrue("value 1", o.bar)
+
+        fields["foo"] = 2
+        fields["bar"] = "value 2"
+        row_count = await orm_class.query.set(fields).eq_pk(pk).update()
+        self.assertEqual(1, row_count)
+
+        o2 = await orm_class.query.eq_pk(pk).one()
+        self.assertEqual(2, o2.foo)
+        self.assertEqual("value 2", o2.bar)
+
+    async def test_update_bubble_up(self):
+        """
+        https://github.com/jaymon/prom/issues/11
+        """
+        orm_class = self.get_orm_class(
+            foo=Field(int),
+            bar=Field(str),
+            _created=None,
+            _updated=None
+        )
+
+        fields = {
+            "foo": 1,
+            "bar": None,
+        }
+        pk = await orm_class.query.set(fields).insert()
+        ret = await (orm_class.query
+            .set_foo(2)
+            .set_bar("bar 2")
+            .ne_bar(None)
+            .update()
+        )
+        self.assertEqual(0, ret)
+
+        ret = await (orm_class.query
+            .set_foo(2)
+            .set_bar("bar 2")
+            .eq_bar(None)
+            .update()
+        )
+        self.assertEqual(1, ret)
+
+    async def test_delete(self):
+        orm_class = self.get_orm_class()
+        pk = await self.insert(orm_class, 1)
+
+        with self.assertRaises(ValueError):
+            r = await orm_class.query.delete()
 
-        pk = Foo.query.select_pk().value_pk(3)
-        self.assertEqual(3, pk)
+        r = await orm_class.query.eq_pk(pk).delete()
+        self.assertEqual(1, r)
+
+        r = await orm_class.query.eq_pk(pk).delete()
+        self.assertEqual(0, r)
+
+    async def test___aiter__(self):
+        count = 5
+        q = self.get_query()
+        await self.insert(q, count)
 
-        pkl = list(Foo.query.select_pk().values_pk([2]))
-        self.assertEqual(2, pkl[0])
+        rcount = 0
+        async for _ in q:
+            rcount += 1
+
+        self.assertEqual(count, rcount)
+
+    def test_set(self):
+        q = self.get_query()
+        field_names = list(q.schema.fields.keys())
+        fields = dict(zip(field_names, [None] * len(field_names)))
+        q.set(**fields)
+        self.assertEqual(fields, {f.name: f.value for f in q.fields_set})
 
-        o = Foo.query.one_pk(1)
-        self.assertEqual(1, o.pk)
+        q = self.get_query()
+        q.set(fields)
+        self.assertEqual(fields, {f.name: f.value for f in q.fields_set})
 
-        ol = list(Foo.query.get_pk([1]))
-        self.assertEqual(1, ol[0].pk)
+    def test_select_1(self):
+        q = self.get_query()
+        fields_select = list(q.schema.fields.keys())
+        q.select(*fields_select[0:-1])
+        self.assertEqual(fields_select[0:-1], list(q.fields_select.names()))
+
+        q = self.get_query()
+        q.select(fields_select)
+        self.assertEqual(fields_select, list(q.fields_select.names()))
+
+        q = self.get_query()
+        q.select(fields_select[0:-1], fields_select[-1])
+        self.assertEqual(fields_select, list(q.fields_select.names()))
+
+        # make sure chaining works
+        q = self.get_query()
+        q.select(fields_select[0]).select(*fields_select[1:])
+        self.assertEqual(fields_select, list(q.fields_select.names()))
 
     def test_select_all(self):
-        Foo = self.get_orm_class()
-        q = Foo.query.select("*")
+        orm_class = self.get_orm_class()
+        q = orm_class.query.select("*")
         self.assertRegex(q.render(), r"(?m)SELECT\s+\*\s+FROM")
 
     def test_schemas(self):
@@ -185,8 +388,8 @@ def test_schemas(self):
     def test_render(self):
         q = self.get_query()
 
-        q.is_foo(1)
-        q.is_bar("two")
+        q.eq_foo(1)
+        q.eq_bar("two")
         r = q.render()
         self.assertRegex(r, r"foo[^=]+=\s*1")
         self.assertRegex(r, r"bar[^=]+=\s*'two'")
@@ -194,8 +397,8 @@ def test_render(self):
     def test_find_methods_1(self):
         q = self.get_query()
 
-        opm, qm, fn = q.find_methods("eq_foo_bar")
-        opm2, qm2, fn2 = q.find_methods("foo_bar_eq")
+        opm, fn = q.find_methods("eq_foo_bar")
+        opm2, fn2 = q.find_methods("foo_bar_eq")
         self.assertEqual("eq_field", opm.__name__)
         self.assertEqual(opm2.__name__, opm.__name__)
         self.assertEqual("foo_bar", fn)
@@ -216,10 +419,12 @@ def test_find_methods_1(self):
     def test_find_methods_2(self):
         q = self.get_query()
 
-        method_name = "is_{}".format(testdata.random.choice(list(q.schema.fields.keys())))
+        method_name = "eq_{}".format(
+            random.choice(list(q.schema.fields.keys()))
+        )
         r = q.find_methods(method_name)
-        self.assertEqual("is_field", r[0].__name__)
-        self.assertTrue(r[2] in set(q.schema.fields.keys()))
+        self.assertEqual("eq_field", r[0].__name__)
+        self.assertTrue(r[1] in set(q.schema.fields.keys()))
 
         with self.assertRaises(AttributeError):
             q.find_methods("testing")
@@ -233,229 +438,175 @@ def test_find_methods_2(self):
         for t in tests:
             r = q.find_methods(t[0])
             self.assertEqual(t[1][0], r[0].__name__)
-            self.assertEqual(t[1][1], r[2])
-
-    def test_find_methods_3(self):
-        q = Query()
-        om, qm, fn = q.find_methods("one_pk")
-        self.assertEqual(q.eq_field, om)
-        self.assertEqual(q.one, qm)
-        self.assertEqual("pk", fn)
+            self.assertEqual(t[1][1], r[1])
 
-        with self.assertRaises(AttributeError):
-            q.find_methods("foo_pk")
-
-    def test_like(self):
+    async def test_like(self):
         _q = self.get_query()
-        self.insert(_q, 5)
+        await self.insert(_q, 5)
         for bar in ["bar che", "foo bar", "foo bar che"]:
-            self.insert_fields(_q, bar=bar)
+            await self.insert_fields(_q, bar=bar)
 
-        count = _q.copy().like_bar("bar%").count()
+        count = await _q.copy().like_bar("bar%").count()
         self.assertEqual(1, count)
 
-        count = _q.copy().like_bar("%bar").count()
+        count = await _q.copy().like_bar("%bar").count()
         self.assertEqual(1, count)
 
-        count = _q.copy().like_bar("%bar%").count()
+        count = await _q.copy().like_bar("%bar%").count()
         self.assertEqual(3, count)
 
-        count = _q.copy().nlike_bar("bar%").count()
+        count = await _q.copy().nlike_bar("bar%").count()
         self.assertEqual(7, count)
 
-        count = _q.copy().nlike_bar("%bar").count()
+        count = await _q.copy().nlike_bar("%bar").count()
         self.assertEqual(7, count)
 
-        count = _q.copy().nlike_bar("%bar%").count()
+        count = await _q.copy().nlike_bar("%bar%").count()
         self.assertEqual(5, count)
 
-        count = _q.copy().like_bar("bar____").count()
+        count = await _q.copy().like_bar("bar____").count()
         self.assertEqual(1, count)
 
-        count = _q.copy().like_bar("____bar").count()
+        count = await _q.copy().like_bar("____bar").count()
         self.assertEqual(1, count)
 
-        count = _q.copy().like_bar("____bar____").count()
+        count = await _q.copy().like_bar("____bar____").count()
         self.assertEqual(1, count)
 
-    def test_between(self):
-        _q = self.get_query()
-        self.insert(_q, 5)
+    async def test_between(self):
+        q = self.get_query()
+        await self.insert(q, 5)
 
-        q = _q.copy()
-        vals = list(q.select_pk().between_pk(2, 4))
+        vals = [r async for r in q.select_pk().between_pk(2, 4)]
         self.assertEqual(3, len(vals))
         for v in vals:
             self.assertTrue(v >= 2 and v <= 4)
 
-    def test_ref_threading(self):
-        basedir = testdata.create_modules({
-            "rtfoo.rtbar.tqr1": [
-                "import prom",
-                "",
-                "class Foo(prom.Orm):",
-                "    table_name = 'thrd_qr2_foo'",
-                "    one=prom.Field(int, True)",
-                "",
-            ],
-            "rtfoo.rtbar.tqr2": [
-                "import prom",
-                "from rtfoo.rtbar.tqr1 import Foo",
-                "",
-                "class Bar(prom.Orm):",
-                "    table_name = 'thrd_qr2_bar'",
-                "    one=prom.Field(int, True)",
-                "    foo_id=prom.Field(Foo, True)",
-                ""
-            ],
-        })
-
-        tqr1 = basedir.module("rtfoo.rtbar.tqr1")
-        sys.modules.pop("rtfoo.rtbar.tqr2.Bar", None)
-        #tqr2 = basedir.module("tqr2")
-        def target():
-            q = tqr1.Foo.query.ref("rtfoo.rtbar.tqr2.Bar")
-            f = tqr1.Foo()
-            q = f.query.ref("rtfoo.rtbar.tqr2.Bar")
-
-        t1 = Thread(target=target)
-        # if we don't get stuck in a deadlock this test passes
-        t1.start()
-        t1.join()
-
-    def test_query_ref_1(self):
+    async def test_query_ref_1(self):
         inter = self.get_interface()
-        testdata.create_modules({
-            "qr2": [
-                "import prom",
-                "",
-                "class Foo(prom.Orm):",
-                "    table_name = 'qr2_foo'",
-                "    foo=prom.Field(int, True)",
-                "    bar=prom.Field(str, True)",
-                ""
-                "class Bar(prom.Orm):",
-                "    table_name = 'qr2_bar'",
-                "    foo=prom.Field(int, True)",
-                "    bar=prom.Field(str, True)",
-                "    che=prom.Field(Foo, True)",
-                ""
-            ]
-        })
-        from qr2 import Foo as t1, Bar as t2
+        modpath = self.create_module([
+            "import prom",
+            "",
+            "class t1(prom.Orm):",
+            "    table_name = 'qr2_t1'",
+            "    foo=prom.Field(int, True)",
+            "    bar=prom.Field(str, True)",
+            ""
+            "class t2(prom.Orm):",
+            "    table_name = 'qr2_t2'",
+            "    foo=prom.Field(int, True)",
+            "    bar=prom.Field(str, True)",
+            "    che=prom.Field(t1, True)",
+            ""
+        ])
+
+
+        t1 = modpath.module().t1
         t1.interface = inter
+
+        t2 = modpath.module().t2
         t2.interface = inter
 
-        ti1 = t1.create(foo=11, bar='11')
-        ti12 = t1.create(foo=12, bar='12')
+        ti1 = await self.insert_fields(t1, foo=11, bar='11')
+        ti12 = await self.insert_fields(t1, foo=12, bar='12')
 
-        ti2 = t2.create(foo=21, bar='21', che=ti1.pk)
-        ti22 = t2.create(foo=22, bar='22', che=ti12.pk)
+        ti2 = await self.insert_fields(t2, foo=21, bar='21', che=ti1)
+        ti22 = await self.insert_fields(t2, foo=22, bar='22', che=ti12)
 
         orm_classpath = "{}.{}".format(t2.__module__, t2.__name__)
 
-        l = list(ti1.query.ref(orm_classpath).select_foo().is_pk(ti12.pk).get())
+        q = t1.query.ref(orm_classpath).select_foo().eq_pk(ti12)
+        l = [r async for r in q]
         self.assertEqual(22, l[0])
         self.assertEqual(1, len(l))
 
-        l = list(ti1.query.ref(orm_classpath).select_foo().is_pk(ti1.pk).get())
+        q = t1.query.ref(orm_classpath).select_foo().eq_pk(ti1)
+        l = [r async for r in q]
         self.assertEqual(21, l[0])
         self.assertEqual(1, len(l))
 
-        l = list(ti1.query.ref(orm_classpath).select_foo().is_pk(ti1.pk).get())
+        q = t1.query.ref(orm_classpath).select_foo().eq_pk(ti1)
+        l = [r async for r in q]
         self.assertEqual(21, l[0])
         self.assertEqual(1, len(l))
 
-        l = list(ti1.query.ref(orm_classpath).select_foo().is_pk(ti1.pk).get())
+        q = t1.query.ref(orm_classpath).select_foo().eq_pk(ti1)
+        l = [r async for r in q]
         self.assertEqual(21, l[0])
         self.assertEqual(1, len(l))
 
-        l = list(ti1.query.ref(orm_classpath).select_foo().get())
+        q = t1.query.ref(orm_classpath).select_foo()
+        l = [r async for r in q]
         self.assertEqual(2, len(l))
 
-    def test_query_ref_2(self):
+    async def test_query_ref_2(self):
         inter = self.get_interface()
-        testdata.create_modules({
-            "qre": "\n".join([
-                "import prom",
-                "",
-                "class T1(prom.Orm):",
-                "    table_name = 'qre_t1'",
-                ""
-                "class T2(prom.Orm):",
-                "    table_name = 'qre_t2'",
-                "    t1_id=prom.Field(T1, True)",
-                ""
-                "class T3(prom.Orm):",
-                "    table_name = 'qre_t3'",
-                ""
-            ])
-        })
-        from qre import T1, T2, T3
+        modpath = self.create_module([
+            "import prom",
+            "",
+            "class T1(prom.Orm):",
+            "    table_name = 'qre_t1'",
+            ""
+            "class T2(prom.Orm):",
+            "    table_name = 'qre_t2'",
+            "    t1_id=prom.Field(T1, True)",
+            ""
+            "class T3(prom.Orm):",
+            "    table_name = 'qre_t3'",
+            ""
+        ])
+
+        T1 = modpath.module().T1
         T1.interface = inter
+
+        T2 = modpath.module().T2
         T2.interface = inter
+
+        T3 = modpath.module().T3
         T3.interface = inter
 
-        t1a = T1.create()
-        t1b = T1.create()
-        t2 = T2.create(t1_id=t1a.pk)
+        t1a = await self.insert_fields(T1)
+        t1b = await self.insert_fields(T1)
+        t2 = await self.insert_fields(T2, t1_id=t1a)
 
         classpath = "{}.{}".format(T2.__module__, T2.__name__)
 
-        r = T1.query.ref(classpath).is_pk(t1a.pk).count()
+        r = await T1.query.ref(classpath).eq_pk(t1a).count()
         self.assertEqual(1, r)
 
-        r = T1.query.ref(classpath).is_pk(t1b.pk).count()
+        r = await T1.query.ref(classpath).eq_pk(t1b).count()
         self.assertEqual(0, r)
 
-    def test_null_iterator(self):
-        """you can now pass empty lists to in and nin and not have them throw an
-        error, instead they return an empty iterator"""
-        _q = self.get_query()
-        self.insert(_q, 1)
-
-        q = _q.copy()
-        r = q.in_foo([]).get()
-        self.assertFalse(r)
-        count = 0
-        for x in r:
-            count += 0
-        self.assertEqual(0, count)
-        self.assertEqual(0, len(r))
-
-    def test_field_datetime(self):
+    async def test_field_datetime(self):
         _q = self.get_query()
 
         q = _q.copy()
-        q.is__created(day=int(datetime.datetime.utcnow().strftime('%d')))
-        r = q.get()
+        q.eq__created(day=int(datetime.datetime.utcnow().strftime('%d')))
+        r = [r async for r in q]
         self.assertFalse(r)
 
-        pk = self.insert(q, 1)[0]
+        pk = await self.insert(q, 1)
 
         # get the object out so we can use it to query
-        o = _q.copy().one_pk(pk)
+        o = await _q.copy().eq_pk(pk).one()
         dt = o._created
         day = int(dt.strftime('%d'))
 
-        q = _q.copy()
-        q.in__created(day=day)
-        r = q.get()
+        q = _q.copy().in__created(day=day)
+        r = [r async for r in q]
         self.assertEqual(1, len(r))
 
-        q = _q.copy()
-        q.is__created(day=day)
-        r = q.get()
+        q = _q.copy().eq__created(day=day)
+        r = [r async for r in q]
         self.assertEqual(1, len(r))
 
-        q = _q.copy()
-        q.in__created(day=[day, day + 1])
-        r = q.get()
+        q = _q.copy().in__created(day=[day, day + 1])
+        r = [r async for r in q]
         self.assertEqual(1, len(r))
 
-    def test_pk_fields(self):
-        tclass = self.get_orm_class()
-        q = tclass.query
+    def test_pk_fields_1(self):
+        orm_class = self.get_orm_class()
+        q = orm_class.query
         q.gte_pk(5).lte_pk(1).lt_pk(1).gte_pk(5)
         q.desc_pk()
         q.asc_pk()
@@ -470,168 +621,70 @@ def test_pk_fields(self):
         for set_field in q.fields_set:
             self.assertEqual(set_field.name, "_id")
 
-    def test_get_pk(self):
-        tclass = self.get_orm_class()
-        t = tclass()
-        t.foo = 1
-        t.bar = "bar1"
-        t.save()
-
-        t2 = tclass()
-        t2.foo = 2
-        t2.bar = "bar2"
-        t2.save()
-
-        pks = [t.pk, t2.pk]
-
-        res = tclass.query.get_pk(pks)
-        self.assertEqual(2, len(res))
-        self.assertEqual(list(res.pk), pks)
-
-    def test_value_query(self):
-        _q = self.get_query()
-
-        v = _q.copy().select_foo().value()
-        self.assertEqual(None, v)
-
-        count = 2
-        pks = self.insert(_q, count)
-        o = _q.copy().one_pk(pks[0])
-
-        v = _q.copy().select_foo().is_pk(o.pk).value()
-        self.assertEqual(o.foo, v)
-
-        v = _q.copy().select_foo().select_bar().is_pk(o.pk).value()
-        self.assertEqual(o.foo, v[0])
-        self.assertEqual(o.bar, v[1])
-
-    def test_pk(self):
+    async def test_pk_fields_2(self):
         orm_class = self.get_orm_class()
-        v = orm_class.query.select_pk().one()
+        v = await orm_class.query.select_pk().one()
         self.assertEqual(None, v)
+
         count = 2
-        self.insert(orm_class, count)
+        await self.insert(orm_class, count)
 
-        v = orm_class.query.select_pk().asc_pk().one()
+        v = await orm_class.query.select_pk().asc_pk().one()
         self.assertEqual(1, v)
 
-    def test_pks(self):
+    async def test_pk_fields_3(self):
         orm_class = self.get_orm_class()
-        q = self.get_query()
-        v = list(orm_class.query.select_pk().get())
+
+        v = [r async for r in orm_class.query.select_pk()]
         self.assertEqual(0, len(v))
+
         count = 2
-        self.insert(orm_class, count)
+        await self.insert(orm_class, count)
 
-        v = list(orm_class.query.select_pk().get())
+        v = [r async for r in orm_class.query.select_pk()]
         self.assertEqual(2, len(v))
 
-    def test___iter__(self):
-        count = 5
-        q = self.get_query()
-        self.insert(q, count)
-
-        rcount = 0
-        for t in q:
-            rcount += 1
-
-        self.assertEqual(count, rcount)
-
-    def test_has(self):
-        q = self.get_query()
-        self.assertFalse(q.has())
-
-        count = 1
-        self.insert(q, count)
-        self.assertTrue(q.has())
-
-    def test_all(self):
-        count = 10
-        q = self.get_query()
-        self.insert(q, count)
-
-        # if no limit is set then it should go through all results
-        rcount = 0
-        for r in q.copy().all():
-            rcount += 1
-        self.assertEqual(count, rcount)
-
-        # if there is a limit then all should only go until that limit
-        rcount = 0
-        for r in q.copy().limit(1).all():
-            rcount += 1
-        self.assertEqual(1, rcount)
-
-        # only go until the end of the results
-        rcount = 0
-        for r in q.copy().limit(6).offset(6).all():
-            rcount += 1
-        self.assertEqual(4, rcount)
-
-    def test_in_field(self):
-        q = self.get_query()
-        q.in_foo([])
-        self.assertEqual([], list(q.get()))
+    async def test_in_field_1(self):
+        q = self.get_query().in_foo([])
+        self.assertEqual([], [r async for r in q])
 
-        q = self.get_query()
-        q.in_foo([1, 2])
-        self.assertEqual(q.fields_where[0].value, [1, 2,])
+        q = self.get_query().in_foo([1, 2])
+        self.assertEqual(q.fields_where[0].value, [1, 2])
 
-        q = self.get_query()
-        q.in_foo([1])
+        q = self.get_query().in_foo([1])
         self.assertEqual(q.fields_where[0].value, [1])
 
-        q = self.get_query()
-        q.in_foo([1, 2])
+        q = self.get_query().in_foo([1, 2])
         self.assertEqual(q.fields_where[0].value, [1, 2])
 
-        q = self.get_query()
-        q.in_foo(range(1, 3))
-        self.assertEqual(q.fields_where[0].value, [1, 2,])
-
-        q = self.get_query()
-        q.in_foo((x for x in [1, 2]))
-        self.assertEqual(q.fields_where[0].value, [1, 2,])
-
-    def test_set(self):
-        q = self.get_query()
-        field_names = list(q.schema.fields.keys())
-        fields = dict(zip(field_names, [None] * len(field_names)))
-        q.set(**fields)
-        self.assertEqual(fields, {f.name: f.value for f in q.fields_set})
-
-        q = self.get_query()
-        q.set(fields)
-        self.assertEqual(fields, {f.name: f.value for f in q.fields_set})
-
-    def test_select(self):
-        q = self.get_query()
-        fields_select = list(q.schema.fields.keys())
-        q.select(*fields_select[0:-1])
-        self.assertEqual(fields_select[0:-1], list(q.fields_select.names()))
+        q = self.get_query().in_foo(range(1, 3))
+        self.assertEqual(q.fields_where[0].value, [1, 2])
 
-        q = self.get_query()
-        q.select(fields_select)
-        self.assertEqual(fields_select, list(q.fields_select.names()))
+        q = self.get_query().in_foo((x for x in [1, 2]))
+        self.assertEqual(q.fields_where[0].value, [1, 2])
 
+    async def test_in_field_empty(self):
+        """you can now pass empty lists to in and nin and not have them throw an
+        error, instead they return an empty iterator"""
         q = self.get_query()
-        q.select(fields_select[0:-1], fields_select[-1])
-        self.assertEqual(fields_select, list(q.fields_select.names()))
+        await self.insert(q, 1)
 
-        # make sure chaining works
-        q = self.get_query()
-        q.select(fields_select[0]).select(*fields_select[1:])
-        self.assertEqual(fields_select, list(q.fields_select.names()))
+        r = [r async for r in q.in_foo([])]
+        self.assertFalse(r)
+        count = 0
+        for x in r:
+            count += 0
+        self.assertEqual(0, count)
+        self.assertEqual(0, len(r))
 
     def test_child_magic(self):
-
         orm_class = self.get_orm_class()
         class ChildQuery(Query):
             pass
         orm_class.query_class = ChildQuery
 
         q = orm_class.query
-        q.is_foo(1) # if there is no error, it passed
+        q.eq_foo(1) # if there is no error, it passed
 
         with self.assertRaises(AttributeError):
             q.aksdlfjldks_foo(2)
@@ -650,8 +703,7 @@ def test_properties(self):
         self.assertFalse(q.interface)
 
     def test___getattr__(self):
-        q = self.get_query()
-        q.is_foo(1)
+        q = self.get_query().eq_foo(1)
         self.assertEqual(1, len(q.fields_where))
         self.assertEqual("eq", q.fields_where[0].operator)
 
@@ -660,8 +712,8 @@ def test___getattr__(self):
 
     def test_append_operation(self):
         tests = [
-            ("is_field", ["foo", 1], ["eq", "foo", 1]),
-            ("not_field", ["foo", 1], ["ne", "foo", 1]),
+            ("eq_field", ["foo", 1], ["eq", "foo", 1]),
+            ("ne_field", ["foo", 1], ["ne", "foo", 1]),
             ("lte_field", ["foo", 1], ["lte", "foo", 1]),
             ("lt_field", ["foo", 1], ["lt", "foo", 1]),
             ("gte_field", ["foo", 1], ["gte", "foo", 1]),
@@ -731,84 +783,11 @@ def test_bounds_methods(self):
         q.limit(0)
         self.assertEqual((0, 0), q.bounds.get())
 
-    def test_insert_and_update(self):
-        orm_class = self.get_orm_class()
-        q = orm_class.query
-        o = orm_class(foo=1, bar="value 1")
-        fields = o.to_interface()
-        pk = q.copy().set(fields).insert()
-        o = q.copy().one_pk(pk)
-        self.assertLess(0, pk)
-        self.assertTrue(o._created)
-        self.assertTrue(o._updated)
-
-        fields["foo"] = 2
-        fields["bar"] = "value 2"
-        row_count = q.copy().set(fields).is_pk(pk).update()
-        self.assertEqual(1, row_count)
-
-        o2 = q.copy().one_pk(pk)
-
-        self.assertEqual(2, o2.foo)
-        self.assertEqual("value 2", o2.bar)
-        self.assertEqual(o._created, o2._created)
-        self.assertEqual(o._updated, o2._updated)
-
-    def test_update_bubble_up(self):
-        """
-        https://github.com/jaymon/prom/issues/11
-        """
-        orm = self.get_orm()
-        orm.schema.set_field("che", prom.Field(str, False))
-        orm.foo = 1
-        orm.bar = "bar 1"
-        orm.che = None
-        orm.save()
-
-        ret = orm.query.set_foo(2).set_bar("bar 2").not_che(None).update()
-        self.assertEqual(0, ret)
-
-        ret = orm.query.set_foo(2).set_bar("bar 2").is_che(None).update()
-        self.assertEqual(1, ret)
-
-    def test_delete(self):
-        tclass = self.get_orm_class()
-        first_pk = self.insert(tclass, 1)[0]
-
-        with self.assertRaises(ValueError):
-            r = tclass.query.delete()
-
-        r = tclass.query.is_pk(first_pk).delete()
-        self.assertEqual(1, r)
-
-        r = tclass.query.is_pk(first_pk).delete()
-        self.assertEqual(0, r)
-
-    def test_get_1(self):
-        TestGetTorm = self.get_orm_class()
-        _ids = self.insert(TestGetTorm, 2)
-
-        q = TestGetTorm.query
-        for o in q.get():
-            self.assertEqual(type(o), TestGetTorm)
-            self.assertTrue(o._id in _ids)
-            self.assertFalse(o.is_modified())
-
-    def test_one(self):
-        TestGetOneTorm = self.get_orm_class()
-        _ids = self.insert(TestGetOneTorm, 2)
-
-        q = TestGetOneTorm.query
-        o = q.one()
-        self.assertEqual(type(o), TestGetOneTorm)
-        self.assertTrue(o._id in _ids)
-        self.assertFalse(o.is_modified())
-
     def test_copy(self):
         q1 = self.get_query()
         q2 = q1.copy()
 
-        q1.is_foo(1)
+        q1.eq_foo(1)
         self.assertEqual(1, len(q1.fields_where))
         self.assertEqual(0, len(q2.fields_where))
 
@@ -816,104 +795,83 @@ def test_copy(self):
         self.assertNotEqual(id(q1.fields_where), id(q2.fields_where))
         self.assertNotEqual(id(q1.bounds), id(q2.bounds))
 
-    def test_values_query(self):
-        _q = self.get_query()
-
-        count = 2
-        pks = self.insert(_q, count)
-
-        vals = _q.copy().select_foo().values()
-        self.assertEqual(count, len(vals))
-        for v in vals:
-            self.assertTrue(isinstance(v, int))
-
-        vals = _q.copy().select_foo().select_bar().values()
-        self.assertEqual(count, len(vals))
-        for v in vals:
-            self.assertTrue(isinstance(v, list))
-
-        vals = _q.copy().select_foo().limit(1).values()
-        self.assertEqual(1, len(vals))
-
-    def test_count(self):
-        orm_class = self.get_orm_class()
-        self.insert(orm_class, 10)
-
-        self.assertEqual(5, orm_class.query.offset(5).count())
-        self.assertEqual(5, orm_class.query.limit(5).count())
-        self.assertEqual(10, orm_class.query.count())
-
     def test_or_clause(self):
         q = self.get_query()
         q.eq_foo(1).OR.gte_foo(10).OR.ne_foo(None).eq_bar(1)
         query_str = re.sub(r"\s+", " ", q.render())
-        self.assertTrue(
-            '( "foo" = 1 OR "foo" >= 10 OR "foo" IS NOT NULL ) AND "bar" = 1' in query_str
-        )
-
-    def test_compound_queries(self):
-        o1 = self.get_orm_class(v1=OrmField(int))
-        o2 = self.get_orm_class(v2=OrmField(int))
+        clause = " ".join([
+            '(',
+            '"foo" = 1',
+            'OR "foo" >= 10',
+            'OR "foo" IS DISTINCT FROM NULL',
+            ')',
+            'AND "bar" = 1'
+        ])
+        self.assertTrue(clause in query_str, query_str)
+
+    async def test_compound_queries(self):
+        o1 = self.get_orm_class(v1=Field(int))
+        o2 = self.get_orm_class(v2=Field(int))
 
         for v in [1, 2, 3]:
-            o1.create(v1=v)
+            await self.insert_fields(o1, v1=v)
 
         for v in [2, 3, 4]:
-            o2.create(v2=v)
+            await self.insert_fields(o2, v2=v)
 
         inter = o1.interface
         schema = o1.schema
 
-        ret = o1.query.difference(
+        ret = await o1.query.difference(
             o1.query.select_v1(),
             o2.query.select_v2().eq_v2(2)
-        ).all()
-        self.assertEqual(set([1, 3]), set(ret))
+        ).get()
+        self.assertEqual(set([1, 3]), set([r async for r in ret]))
 
-        ret = o1.query.intersect(
+        ret = await o1.query.intersect(
             o1.query.select_v1(),
             o2.query.select_v2()
         ).count()
         self.assertEqual(2, ret)
 
-        ret = o1.query.intersect(
+        ret = await o1.query.intersect(
             o1.query.select_v1(),
             o2.query.select_v2()
-        ).all()
-        self.assertEqual(set([3, 2]), set(ret))
+        ).get()
+        self.assertEqual(set([3, 2]), set([r async for r in ret]))
 
-        ret = o1.query.union(
+        ret = await o1.query.union(
             o1.query.select_v1().eq_v1(1),
             o2.query.select_v2().eq_v2(2)
-        ).all()
-        self.assertEqual(set([1, 2]), set(ret))
+        ).get()
+        self.assertEqual(set([1, 2]), set([r async for r in ret]))
 
-        ret = o1.query.union(
+        ret = await o1.query.union(
             o1.query.select_v1().eq_v1(1),
             o2.query.select_v2().eq_v2(2)
-        ).limit(1).all()
-        self.assertEqual(1, len(ret))
+        ).limit(1).get()
+        self.assertEqual(1, len([r async for r in ret]))
 
-    def test_model_name(self):
+    async def test_model_name(self):
         o1_class = self.get_orm_class(
-            bar=OrmField(str),
+            bar=Field(str),
             model_name="o1qmodel",
         )
         o2_class = self.get_orm_class(
-            o1_id=OrmField(o1_class),
+            o1_id=Field(o1_class),
             model_name="o2qmodel",
         )
 
-        o1 = self.insert_orm(o1_class, bar="1")
-        o2 = self.insert_orm(o2_class, o1_id=o1.pk)
+        o1 = await self.insert_orm(o1_class, bar="1")
+        o2 = await self.insert_orm(o2_class, o1_id=o1.pk)
 
-        o2r = o2_class.query.eq_o1qmodel(o1).one()
+        o2r = await o2_class.query.eq_o1qmodel(o1).one()
         self.assertEqual(o2.pk, o2r.pk)
 
-        o2r = o2_class.query.eq_field(o1.model_name, o1).one()
+        o2r = await o2_class.query.eq_field(o1.model_name, o1).one()
         self.assertEqual(o2.pk, o2r.pk)
 
-        o2r = o2_class.query.eq_field(o1.models_name, o1).one()
+        o2r = await o2_class.query.eq_field(o1.models_name, o1).one()
         self.assertEqual(o2.pk, o2r.pk)
 
     def test_raw_field_1(self):
@@ -926,275 +884,274 @@ def test_raw_field_1(self):
 
         self.assertTrue("MAX(foo) = 1" in q)
 
+    async def test_sort_order(self):
+        q = self.get_query()
+        await self.insert(q, 10)
+
+        foos = [r async for r in q.copy().select_foo().asc_pk()]
+        foos.sort()
+
+        for x in range(2, 9):
+            q3 = q.copy().select_foo().asc_foo().limit(1).page(x)
+            rows = [r async for r in q3]
+            self.assertEqual(foos[x - 1], rows[0])
+
+            row = await q.copy().select_foo().asc_foo().limit(1).page(x).one()
+            self.assertEqual(foos[x - 1], row)
+
+            q3 = await q.copy().select_foo().asc_foo().limit(1).page(x).one()
+            self.assertEqual(foos[x - 1], row)
+
+            q3 = q.copy().select_foo().in_foo(foos)
+            q3.asc_foo(foos).limit(1).page(x)
+            rows = [r async for r in q3]
+            self.assertEqual(foos[x - 1], rows[0])
+
+            q3 = q.copy().select_foo().in_foo(foos)
+            q3.asc_foo(foos).limit(1).page(x)
+            row = await q3.one()
+            self.assertEqual(foos[x - 1], row)
+
+        for x in range(1, 9):
+            q3 = q.copy().select_foo().asc_foo().limit(x).offset(x)
+            rows = [r async for r in q3]
+            self.assertEqual(foos[x], rows[0])
+
+            q3 = q.copy()
+            row = await q.copy().select_foo().asc_foo().limit(x).offset(x).one()
+            self.assertEqual(foos[x], row)
+
+            row = await q.copy().select_foo().asc_foo().limit(x).offset(x).one()
+            self.assertEqual(foos[x], row)
+
+            q3 = q.copy().select_foo().in_foo(foos)
+            q3.asc_foo(foos).limit(1).offset(x)
+            rows = [r async for r in q3]
+            self.assertEqual(foos[x], rows[0])
+
+            q3 = q.copy().select_foo().in_foo(foos)
+            q3.asc_foo(foos).limit(1).offset(x)
+            row = await q3.one()
+            self.assertEqual(foos[x], row)
+
+    async def test_sort_list(self):
+        q = self.get_query()
+        await self.insert(q, 10)
+
+        q2 = q.copy()
+        foos = [r async for r in q.copy().select_foo()]
+        random.shuffle(foos)
+
+        rows = await q.copy().select_foo().in_foo(foos).asc_foo(foos).tolist()
+        for i, r in enumerate(rows):
+            self.assertEqual(foos[i], r)
+
+        rfoos = list(reversed(foos))
+        rows = await q.copy().select_foo().in_foo(foos).desc_foo(foos).tolist()
+        for i, r in enumerate(rows):
+            self.assertEqual(rfoos[i], r)
+
+        q3 = q.copy()
+        rows = await q3.in_foo(foos).asc_foo(foos).limit(2).offset(2).tolist()
+        for i, r in enumerate(rows, 2):
+            self.assertEqual(foos[i], r.foo)
+
+        # now test a string value
+        qb = q.copy()
+        bars = await q.copy().select_bar().tolist()
+        random.shuffle(bars)
+
+        qb = q.copy()
+        rows = await q.copy().in_bar(bars).asc_bar(bars).tolist()
+        for i, r in enumerate(rows):
+            self.assertEqual(bars[i], r.bar)
+
+        # make sure limits and offsets work
+        qb = q.copy()
+        rows = await q.copy().in_bar(bars).asc_bar(bars).limit(5).tolist()
+        for i, r in enumerate(rows):
+            self.assertEqual(bars[i], r.bar)
+
+        qb = q.copy()
+        rows = await qb.in_bar(bars).asc_bar(bars).limit(2).offset(2).tolist()
+        for i, r in enumerate(rows, 2):
+            self.assertEqual(bars[i], r.bar)
+
+        # make sure you can select on one row and sort on another
+        vs = await q.copy().select_foo().select_bar().tolist()
+        random.shuffle(vs)
+
+        rows = await q.copy().select_foo().asc_bar((v[1] for v in vs)).tolist()
+        for i, r in enumerate(rows):
+            self.assertEqual(vs[i][0], r)
+
 
 class IteratorTest(EnvironTestCase):
-    def get_iterator(self, count=5, limit=5, page=0):
+    async def get_iterator(self, count=5, limit=5, page=0):
         q = self.get_query()
-        self.insert(q, count)
-        i = q.limit(limit).page(page).get()
+        await self.insert(q, count)
+        i = await q.limit(limit).page(page).get()
         return i
 
-    def test___repr__(self):
+    async def test___repr__(self):
         """https://github.com/Jaymon/prom/issues/137"""
-        orm_class = self.create_orms()
-
-        it = orm_class.query.get()
+        orm_class = await self.create_orms()
+        it = await orm_class.query.get()
         s = it.__repr__()
         self.assertNotEqual("[]", s)
 
-    def test___init__(self):
-        count = 10
+    async def test___init__(self):
+        index = 4
         orm_class = self.get_orm_class()
-        self.insert(orm_class, count)
-
-        q = orm_class.query.gt_pk(5)
+        pks = await self.insert(orm_class, 10)
 
-        it = Iterator(q)
-        self.assertLess(0, len(it))
+        it = await orm_class.query.gt_pk(pks[index]).get()
+        self.assertLess(0, await it.count())
 
-        for o in it:
-            self.assertLess(5, o.pk)
+        async for o in it:
+            self.assertLess(pks[index], o.pk)
 
-    def test___getitem___slicing(self):
-        count = 10
+    async def test___getitem___slicing(self):
         orm_class = self.get_orm_class()
-        pks = self.insert(orm_class, count)
+        pks = await self.insert(orm_class, 10)
 
-        it = orm_class.query.select_pk().asc_pk().get()
+        it = await orm_class.query.select_pk().asc_pk().get()
 
-        self.assertEqual(pks[-5:6], list(it[-5:6]))
-        self.assertEqual(pks[2:5], list(it[2:5]))
+        self.assertEqual(pks[-5:6], await (await it[-5:6]).tolist())
+        self.assertEqual(pks[2:5], await (await it[2:5]).tolist())
 
-        self.assertEqual(pks[2:], list(it[2:]))
-        self.assertEqual(pks[:2], list(it[:2]))
+        self.assertEqual(pks[2:], await (await it[2:]).tolist())
+        self.assertEqual(pks[:2], await (await it[:2]).tolist())
 
         with self.assertRaises(ValueError):
-            it[1:2:2]
+            await it[1:2:2]
 
-    def test___getitem___positive_index(self):
-        count = 10
+    async def test___getitem___positive_index(self):
         orm_class = self.get_orm_class()
-        orm_class.install()
-        pks = self.insert(orm_class, count)
+        pks = await self.insert(orm_class, 10)
 
-        q = orm_class.query.asc_pk()
-        it = Iterator(q)
+        it = await orm_class.query.asc_pk().get()
 
-        self.assertEqual(pks[0], it[0].pk)
-        self.assertEqual(pks[-1], it[len(pks) - 1].pk)
+        self.assertEqual(pks[0], (await it[0]).pk)
+        self.assertEqual(pks[-1], (await it[len(pks) - 1]).pk)
         with self.assertRaises(IndexError):
-            it[len(pks)]
+            await it[len(pks)]
 
-        q = orm_class.query.offset(4).limit(2).asc_pk()
-        it = Iterator(q)
-        self.assertEqual(pks[4], it[0].pk)
-        self.assertEqual(pks[5], it[1].pk)
+        it = await orm_class.query.offset(4).limit(2).asc_pk().get()
+        self.assertEqual(pks[4], (await it[0]).pk)
+        self.assertEqual(pks[5], (await it[1]).pk)
         with self.assertRaises(IndexError):
-            it[3]
+            await it[3]
 
-    def test___getitem___negative_index(self):
-        count = 10
+    async def test___getitem___negative_index(self):
         orm_class = self.get_orm_class()
-        pks = self.insert(orm_class, count)
-
-        q = orm_class.query.asc_pk()
-        it = Iterator(q)
+        pks = await self.insert(orm_class, 10)
 
-        self.assertEqual(it[-1].pk, pks[-1])
-        self.assertEqual(it[-2].pk, pks[-2])
+        it = await orm_class.query.asc_pk().get()
+        self.assertEqual(pks[-1], (await it[-1]).pk)
+        self.assertEqual(pks[-2], (await it[-2]).pk)
         with self.assertRaises(IndexError):
-            it[-(len(pks) + 5)]
-
-    def test_copy(self):
-        count = 10
-        orm_class = self.get_orm_class()
-        self.insert(orm_class, count)
-
-        q = orm_class.query.asc_pk()
-        it1 = Iterator(q)
-        it2 = it1.copy()
-        it2.reverse()
-        self.assertNotEqual(list(v for v in it1), list(v for v in it2))
+            await it[-(len(pks) + 5)]
 
-    def test_custom(self):
+    async def test_custom(self):
         """make sure setting a custom Iterator class works normally and wrapped
         by an AllIterator()"""
         count = 3
         orm_class = self.get_orm_class()
-        self.insert(orm_class, count)
-
-        self.assertEqual(count, len(list(orm_class.query.get())))
+        await self.insert(orm_class, count)
 
         class CustomIterator(Iterator):
             def ifilter(self, o):
                 return not o.pk == 1
         orm_class.iterator_class = CustomIterator
 
-        self.assertEqual(count - 1, len(list(orm_class.query.get())))
-        self.assertEqual(count - 1, len(list(orm_class.query.all())))
-
-    def test_ifilter(self):
-        count = 3
-        _q = self.get_query()
-        self.insert(_q, count)
-
-        l = _q.copy().get()
-        self.assertEqual(3, len(list(l)))
-
-        l = _q.copy().get()
-        def ifilter(o): return o.pk == 1
-        l.ifilter = ifilter
-        l2 = _q.copy().get()
-        self.assertEqual(len(list(filter(ifilter, l2))), len(list(l)))
+        it = await orm_class.query.tolist()
+        self.assertEqual(count - 1, len(await orm_class.query.tolist()))
 
-    def test_reverse(self):
-        """Iterator.reverse() reverses the iterator in place"""
-        count = 10
-        orm_class = self.get_orm_class()
-        pks = self.insert(orm_class, count)
-        pks.reverse()
-
-        q = orm_class.query.asc_pk()
-        it = Iterator(q)
-        it.reverse()
-        for i, o in enumerate(it):
-            self.assertEqual(pks[i], o.pk)
-
-        q = orm_class.query.asc_pk()
-        it = Iterator(q)
-        for i, o in enumerate(reversed(it)):
-            self.assertEqual(pks[i], o.pk)
-
-    def test_all_1(self):
-        count = 15
+    async def test_ifilter(self):
         q = self.get_query()
-        pks = self.insert(q, count)
-        self.assertLess(0, len(pks))
-        g = q.all()
+        await self.insert(q, 3)
 
-        self.assertEqual(1, g[0].pk)
-        self.assertEqual(2, g[1].pk)
-        self.assertEqual(3, g[2].pk)
-        self.assertEqual(6, g[5].pk)
-        self.assertEqual(13, g[12].pk)
+        l = await q.copy().tolist()
+        self.assertEqual(3, len(l))
 
-        with self.assertRaises(IndexError):
-            g[count + 5]
-
-        for i, x in enumerate(g):
-            if i > 7: break
-        self.assertEqual(9, g[8].pk)
-
-        gcount = 0
-        for x in g: gcount += 1
-        self.assertEqual(count, gcount)
-
-        gcount = 0
-        for x in g: gcount += 1
-        self.assertEqual(count, gcount)
-
-        self.assertEqual(count, len(g))
-
-        g = q.all()
-        self.assertEqual(count, len(g))
-
-    def test_all_limit(self):
-        count = 15
-        q = self.get_query()
-        self.insert(q, count)
-        q.limit(5)
-        g = q.all()
+        def ifilter(o): return o.pk == 1
 
-        self.assertEqual(3, g[2].pk)
-        with self.assertRaises(IndexError):
-            g[6]
+        l = await q.copy().tolist()
+        l2 = await q.copy().ifilter(ifilter).tolist()
+        self.assertEqual(len(list(filter(ifilter, l))), len(l2))
 
-    def test_values(self):
+    async def test_select_fields(self):
         count = 5
-        _q = self.get_query()
-        self.insert(_q, count)
+        q = self.get_query()
+        await self.insert(q, count)
 
-        g = _q.copy().select_bar().get()
+        g = await q.copy().select_bar().get()
         icount = 0
-        for v in g:
+        async for v in g:
             self.assertTrue(isinstance(v, basestring))
             icount += 1
         self.assertEqual(count, icount)
 
-        g = _q.copy().select_bar().select_foo().get()
+        g = await q.copy().select_bar().select_foo().get()
         icount = 0
-        for v in g:
+        async for v in g:
             icount += 1
             self.assertTrue(isinstance(v[0], basestring))
             self.assertTrue(isinstance(v[1], int))
         self.assertEqual(count, icount)
 
-    def test___iter__(self):
+    async def test___aiter__(self):
         count = 5
-        i = self.get_iterator(count)
+        i = await self.get_iterator(count)
 
         rcount = 0
-        for t in i:
+        async for t in i:
             rcount += 1
         self.assertEqual(count, rcount)
 
-        rcount = 0
-        for t in i:
-            self.assertTrue(isinstance(t, prom.Orm))
-            rcount += 1
-        self.assertEqual(count, rcount)
+        with self.assertRaises(ValueError):
+            async for t in i:
+                pass
 
-    def test___len__(self):
+    async def test_count(self):
         count = 5
-        i = self.get_iterator(count)
-        self.assertEqual(len(i), count)
+        i = await self.get_iterator(count)
+        self.assertEqual(count, await i.count())
 
         orm_class = i.orm_class
 
-        i = orm_class.query.limit(3).get()
-        self.assertEqual(3, len(i))
-
-    def test___getattr__(self):
-        count = 5
-        i = self.get_iterator(count)
-        rs = list(i.foo)
-        self.assertEqual(count, len(rs))
+        i = await orm_class.query.limit(3).get()
+        self.assertEqual(3, await i.count())
 
-        with self.assertRaises(AttributeError):
-            i.kadfjkadfjkhjkgfkjfkjk_bogus_field
-
-    def test_pk(self):
-        count = 5
-        i = self.get_iterator(count)
-        rs = list(i.pk)
-        self.assertEqual(count, len(rs))
-
-    def test_has_more_1(self):
+    async def test_has_more_1(self):
         limit = 3
         count = 5
         q = self.get_query()
-        pks = self.insert(q.orm_class, count)
-        self.assertEqual(count, len(pks))
-
-        i = q.limit(limit).page(0).get()
+        pks = await self.insert(q, count)
 
-        self.assertTrue(i.has_more())
+        i = await q.limit(limit).page(0).get()
+        self.assertTrue(await i.has_more())
+        await i.close()
 
-        i = q.limit(limit).page(3).get()
-        self.assertFalse(i.has_more())
+        i = await q.limit(limit).page(3).get()
+        self.assertFalse(await i.has_more())
+        await i.close()
 
-        i = q.limit(limit).page(1).get()
-        self.assertTrue(i.has_more())
+        i = await q.limit(limit).page(1).get()
+        self.assertTrue(await i.has_more())
+        await i.close()
 
-        i = q.limit(0).page(0).get()
-        self.assertFalse(i.has_more())
+        i = await q.limit(0).page(0).get()
+        self.assertFalse(await i.has_more())
+        await i.close()
 
-    def test_has_more_limit(self):
+    async def test_has_more_limit(self):
         limit = 4
         count = 10
         q = self.get_query()
-        pks = self.insert(q, count)
+        pks = await self.insert(q, count)
 
-        it = q.select_pk().limit(limit).asc_pk().get()
-        self.assertEqual(pks[:limit], list(it))
+        rpks = await q.select_pk().limit(limit).asc_pk().tolist()
+        self.assertEqual(pks[:limit], rpks)
 
diff --git a/tests/testdata.py b/tests/testdata.py
new file mode 100644
index 0000000..2352cd4
--- /dev/null
+++ b/tests/testdata.py
@@ -0,0 +1,488 @@
+# -*- coding: utf-8 -*-
+import os
+import random
+import string
+import datetime
+import decimal
+
+# make this module act like global testdata
+from testdata import *
+from testdata import __getattr__
+
+from prom.compat import *
+from prom.query import Query
+from prom.model import Orm
+from prom.config import Schema, DsnConnection, Field, Index, AutoIncrement
+from prom.interface import find_environ
+from prom.utils import make_dict
+
+
+class InterfaceData(TestData):
+    """Hold all helper methods to create db tables and get tests ready
+
+    These testdata methods are specifically to test prom itself and should
+    never be used outside of prom's tests
+    """
+
+    interfaces = set()
+    """Every interface that is created gets added to this set so that it can be
+    closed"""
+
+    def get_interfaces(self):
+        """Return all currently configured interfaces in a list"""
+        return list(self.interfaces)
+
+    def get_interface(self, interface=None):
+        """Get an interface, this does not connect the interface
+
+        :param Interface: Interface, if you pass one in that will be returned
+            instead of a new interface created
+        :returns: Interface
+        """
+        return interface or self.create_interface()
+
+    def create_interface(self):
+        """Create an interface and return it, this does not connect the
+        interface
+
+        NOTE -- this uses a class property `interface_class` that needs to be
+        set in a test setup method like TestCase.setUp or the like, if that
+        class property is set then it will use that class to create the
+        interface, otherwise it just creates the first found interface
+
+        :return: Interface
+        """
+        if interface_class := getattr(self, "interface_class", None):
+            return self.find_interface(interface_class)
+
+        else:
+            for inter in self.create_environ_interfaces():
+                return inter
+
+    def create_dsn_interface(self, dsn):
+        """Create an interface with the given DSN
+
+        :param dsn: str, the DSN string
+        :returns: Interface
+        """
+        conn = DsnConnection(dsn)
+        inter = conn.interface
+        self.interfaces.add(inter)
+        return inter
+
+    def create_environ_connections(self, dsn_env_name="PROM_TEST_DSN"):
+        """creates all the connections that are defined in the environment under
+        <dsn_env_name>_N where N can be any integer
+
+        NOTE -- since this can create multiple connections if the environment
+        variables are postfixed with _N (eg, *_1, *_2) you can set the
+        environment variable `PROM_TEST_DSN_INDEX` to an index to limit
+        connection creation to only that index, so to limit to just *_1:
+
+            export PROM_TEST_DSN_INDEX=1
+
+        :param dsn_env_name: str, the name of the environment variable that 
+            should be read to create the connections
+        :returns: generator[Connection]
+        """
+        found = False
+        if dsn_index := os.environ.get("PROM_TEST_DSN_INDEX", 0):
+            for conn in find_environ(f"{dsn_env_name}_{dsn_index}"):
+                found = True
+                yield conn
+
+        else:
+            for conn in find_environ(dsn_env_name):
+                found = True
+                yield conn
+
+        if not found:
+            raise ValueError("No connection found, set PROM_TEST_DSN")
+
+    def create_environ_interfaces(self):
+        """find any interfaces that match dsn_env_name and yield them
+
+        This is basically .create_environ_connections but wraps the connection
+        instance in an Interface instance
+
+        :returns: generator[Interface]
+        """
+        for conn in self.create_environ_connections():
+            inter = conn.interface
+            self.interfaces.add(inter)
+            yield inter
+
+    def get_environ_interface_classes(self):
+        """Returns all the Interface classes of the given environment
+
+        :returns: generator[type], when you don't want the Connection instance,
+            you don't want an Interface instance, you just want the Connection
+            class
+        """
+        for conn in self.create_environ_connections():
+            yield conn.interface_class
+
+    def find_interface(self, interface_class):
+        """Find an Interface that matches `interface_class`
+
+        :returns: Interface
+        """
+        for inter in self.create_environ_interfaces():
+            if isinstance(inter, interface_class):
+                return inter
+
+    def get_table_name(self, table_name=None, prefix=""):
+        """return a random table name
+
+        :param table_name: str, if passed in then this will be returned
+        :param prefix: str, if passed in then a table name will be randomly
+            generated that starts with this
+        :returns: str, a usable table name
+        """
+        if table_name:
+            return table_name
+
+        return "{}{}_table".format(
+            prefix,
+            "".join(
+                random.sample(string.ascii_lowercase, random.randint(5, 15))
+            )
+        )
+
+    def get_orm_class(self, table_name=None, prefix="orm_class", **properties):
+        """Return an orm class
+
+        :param table_name: str, the table the orm class will wrap
+        :param prefix: str, the class name will start with this
+        :param **properties: these will be set on the orm class, if they are
+            Field instances then those will be set as schema fields, if they
+            are Index instances they will be schema indexes
+            - interface: Interface, the interface the returned class will use
+            - connection_name: str, an interface will be created with this
+                connection name
+        :returns: type, an Orm child class
+        """
+        tn = self.get_table_name(table_name, prefix=prefix)
+        parent_class = properties.get("parent_class", Orm)
+
+        properties["table_name"] = tn
+
+        if "interface" not in properties:
+            if "connection_name" not in properties:
+                properties["interface"] = self.get_interface()
+
+        has_field = False
+        for k, v in properties.items():
+            if isinstance(v, Field):
+                has_field = True
+                break
+
+            elif isinstance(v, type) and issubclass(v, Field):
+                has_field = True
+                break
+
+        if not has_field:
+            properties.update({
+                "foo": Field(int, True),
+                "bar": Field(str, True),
+                "ifoobar": Index("foo", "bar"),
+            })
+
+        orm_class = type(
+            String(tn),
+            (parent_class,),
+            properties,
+        )
+
+        return orm_class
+
+    def get_orm(self, table_name=None, prefix="orm", **fields):
+        """Returns an Orm instance
+
+        :param table_name: passed through to `.get_orm_class`
+        :param prefix: passed through to `.get_orm_class`
+        :param **fields: these are used to instantiate an instance of the
+            created orm class
+        :returns: Orm
+        """
+        orm_class = self.get_orm_class(table_name, prefix=prefix)
+        t = orm_class(**fields)
+        return t
+
+    async def create_orms(self, table_name=None, count=0, **fields):
+        """Create count orms at table_name with fields
+
+        see `.get_orm_class` for the arguments
+
+        :param table_name: passed through to `.get_orm_class`
+        :param prefix: passed through to `.get_orm_class`
+        :param **fields: passed through to `.get_orm_class`
+        :param count: int, how many orms you want created
+        :returns: type, the Orm class, the Orm class created with table_name
+        """
+        orm_class = self.get_orm_class(table_name, **fields)
+        count = count or self.get_int(1, 10)
+        await self.insert(orm_class, count)
+        return orm_class
+
+    async def create_orm(self, table_name=None, **fields):
+        """Creat an orm with the given field values
+
+        :param table_name: passed through to `.get_orm`
+        :param prefix: passed through to `.get_orm`
+        :param **fields: passed through to `.get_orm`
+        :returns: Orm, this will be persisted in the db
+        """
+        orm_class = self.get_orm(table_name, **fields)
+        fs = self.get_fields(orm_class.schema)
+        return await orm_class.create(fs)
+
+    def get_table(self, interface=None, table_name=None, **fields_or_indexes):
+        """Return an interface and schema for a table in the db
+
+        This does not try and create anything
+
+        :param interface: Interface, the interface you want to use
+        :param table_name: str, the table name, uses .get_table_name() if None
+        :param **fields_or_indexes: keys will be field name and the values
+            should be Field or Index instances
+        :returns: tuple[Interface, Schema]
+        """
+        interface = self.get_interface(interface)
+        schema = self.get_schema(table_name, **fields_or_indexes)
+        return interface, schema
+
+    async def create_table(self, *args, **kwargs):
+        """Create a table on the db
+
+        :param *args: passed through to `.get_table`
+        :param **kwargs: passed through to `.get_table`
+        :returns: tuple[Interface, Schema]
+        """
+        interface, schema = self.get_table(*args, **kwargs)
+        await interface.set_table(schema)
+        return interface, schema
+
+    def get_schema(self, table_name=None, prefix="schema", **fields_or_indexes):
+        """Get a Schema instance
+
+        :param table_name: str, the table name you want the schema to have, if
+            not given it will be automatically created using `prefix`
+        :param prefix`: str, used to create the table name
+        :param **fields_or_indexes: fields will be Field instances with the key
+            being the name. Indexes will be Index instances with key as the
+            index name
+        :returns: Schema
+        """
+        if not fields_or_indexes:
+            fields_or_indexes.setdefault("foo", Field(int, True))
+            fields_or_indexes.setdefault("bar", Field(str, True))
+            fields_or_indexes.setdefault("ifoobar", Index("foo", "bar"))
+
+        fields_or_indexes.setdefault("_id", AutoIncrement())
+
+        # remove any None values
+        for k in list(fields_or_indexes.keys()):
+            if not fields_or_indexes[k]:
+                fields_or_indexes.pop(k)
+
+        s = Schema(
+            self.get_table_name(table_name, prefix=prefix),
+            **fields_or_indexes
+        )
+        return s
+
+    def get_schema_all(self, inter=None):
+        """return a schema that has a field for all supported standard field
+        types"""
+
+        # this is for foreign key fields
+        orm_class = self.get_orm_class()
+        if inter:
+            orm_class.interface = inter
+            orm_class.install()
+
+        s = Schema(
+            self.get_table_name(),
+            _id=AutoIncrement(),
+            a_bool_y=Field(bool, True),
+            a_bool_n=Field(bool, False),
+            a_sint_y=Field(int, True, size=50),
+            a_sint_n=Field(int, False, size=50),
+            a_dec_y=Field(decimal.Decimal, True),
+            a_dec_n=Field(decimal.Decimal, False),
+            a_float_y=Field(float, True, size=10),
+            a_float_n=Field(float, False, size=10),
+            a_long_y=Field(long, True),
+            a_long_n=Field(long, False),
+            a_fk_y=Field(orm_class, True),
+            a_fk_n=Field(orm_class, False),
+            a_dt_y=Field(datetime.datetime, True),
+            a_dt_n=Field(datetime.datetime, False),
+            a_d_y=Field(datetime.date, True),
+            a_d_n=Field(datetime.date, False),
+            a_int_y=Field(int, True),
+            a_int_n=Field(int, False),
+            a_str_y=Field(str, True),
+            a_str_n=Field(str, False),
+            a_vchar_y=Field(str, True, max_size=512),
+            a_vchar_n=Field(str, False, max_size=512),
+            a_char_y=Field(str, True, size=32),
+            a_char_n=Field(str, False, size=32),
+        )
+
+        return s
+
+    def get_query(self, table_name=None, prefix="query"):
+        """Get a query instance
+
+        :param table_name: str, if passed in the table will be named this
+        :param prefix: str, a random table name will be created using this
+            prefix
+        :returns: Query, this query will belong to an Orm, so Query.orm_class
+            should return the orm class
+        """
+        orm_class = self.get_orm_class(table_name, prefix=prefix)
+        return orm_class.query
+
+    def get_fields(self, schema, **field_kwargs):
+        """return the fields of orm with randomized data
+
+        :param schema: Schema, values for all the fields in this schema will be
+            randomly generated
+        :param **field_kwargs: specific field values you want the returned dict
+            to have
+        :returns: dict[str, Any]
+        """
+        fields = {}
+        for k, v in schema.fields.items():
+            if v.is_pk(): continue
+            if v.is_ref(): continue
+
+            if issubclass(v.interface_type, basestring):
+                fields[k] = self.get_words()
+
+            elif issubclass(v.interface_type, bool):
+                fields[k] = True if random.randint(0, 1) == 1 else False
+
+            elif issubclass(v.interface_type, int):
+                fields[k] = self.get_int32()
+
+            elif issubclass(v.interface_type, long):
+                fields[k] = self.get_int64()
+
+            elif issubclass(v.interface_type, datetime.datetime):
+                fields[k] = self.get_past_datetime()
+
+            elif issubclass(v.interface_type, float):
+                fields[k] = self.get_float()
+
+            else:
+                raise ValueError("{}".format(v.interface_type))
+
+        fields.update(field_kwargs)
+        return fields
+
+    def get_insert_fields(self, *args, **kwargs):
+        """Gets everything needed to insert fields into a table
+
+        Internal method used by the other .insert* methods
+
+        :param *args:
+            - interface, schema, count
+            - query, count
+            - (interface, schema), count
+            - orm_class, count
+            - orm instance, count
+            - orm_class
+            - orm instance
+        :returns: tuple[Interface, Schema, Orm, list[dict]], the list will be
+            count dicts with fields to insert using Schema
+        """
+        if len(args) == 1:
+            o = args[0]
+            count = 1
+
+        elif len(args) == 2:
+            o = args[0]
+            count = args[1]
+
+        elif len(args) == 3:
+            o = (args[0], args[1])
+            count = args[2]
+
+        else:
+            raise ValueError(f"Passed in {len(args)} arguments")
+
+        if isinstance(o, Query):
+            orm_class = o.orm_class
+            interface = o.interface
+            schema = orm_class.schema
+
+        elif isinstance(o, tuple):
+            orm_class = None
+            interface, schema = o
+
+        elif isinstance(o, Orm):
+            orm_class = type(o)
+            schema = orm_class.schema
+            interface = o.interface
+
+        elif issubclass(o, Orm):
+            orm_class = o
+            schema = orm_class.schema
+            interface = o.interface
+
+        else:
+            raise ValueError("couldn't get fields for object {}".format(o))
+
+        fields_list = []
+        for i in range(count):
+            fields_list.append(self.get_fields(schema, **kwargs))
+
+        return interface, schema, orm_class, fields_list
+
+    async def insert(self, *args, **kwargs):
+        """This has the same signature as .get_insert_fields but inserts the
+        generated fields into the db
+
+        :param *args: passed through to .get_insert_fields
+        :param **kwargs: passed through to .get_insert_fields
+        :returns: int|list, if only one row was inserted then this will return
+            the pk of that row, if multiple rows were inserted it will return
+            a list of primary keys for all the rows inserted
+        """
+        pks = []
+
+        interface, schema, orm_class, fields = self.get_insert_fields(
+            *args,
+            **kwargs
+        )
+        for fs in fields:
+            pks.append(await interface.insert(schema, fs))
+
+        return pks if len(pks) > 1 else pks[0]
+
+    async def insert_fields(self, o, fields=None, **fields_kwargs):
+        """Insert fields
+
+        :param o: Query|tuple[Interface, Schema]|Orm
+        :param fields: dict|None
+        :param **fields_kwargs: merged with fields
+        :returns: Any, the primary key for the row containing the inserted
+            fields
+        """
+        fields = make_dict(fields, fields_kwargs)
+        return await self.insert(o, 1, **fields)
+
+    async def insert_orm(self, orm_class, fields=None, **fields_kwargs):
+        """Insert fields into orm_class and return an orm instance
+
+        :param orm_class: Orm, the orm class
+        :param fields: dict|None
+        :param **fields_kwargs: merged with fields
+        :returns: Orm, an instance of orm_class that represents a row in the db
+        """
+        pk = await self.insert_fields(orm_class, fields, **fields_kwargs)
+        return await orm_class.query.eq_pk(pk).one()
+
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 3781162..1720e9f 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -1,9 +1,6 @@
 # -*- coding: utf-8 -*-
-from __future__ import unicode_literals, division, print_function, absolute_import
 
-import testdata
-
-from . import BaseTestCase, TestCase
+from . import TestCase
 from prom.utils import (
     get_objects,
     make_list,
@@ -12,7 +9,7 @@
 
 class GetObjectsTest(TestCase):
     def test_relative_import(self):
-        basedir = testdata.create_modules({
+        basedir = self.create_modules({
             "relimp.foo.bar": [
                 "class Bar(object): pass",
                 "",
@@ -37,28 +34,31 @@ def test_relative_import(self):
         })
 
         with self.assertRaises(ImportError):
-            module, klass = get_objects('...too.far', "relimp.foo.bar")
+            module, klass = get_objects("...too.far", "relimp.foo.bar")
 
-        module, klass = get_objects('...baz.Baz', "relimp.foo.bar")
+        module, klass = get_objects("...baz.Baz", "relimp.foo.bar")
         self.assertEqual("relimp.baz", module.__name__)
         self.assertEqual("Baz", klass.__name__)
 
-        module, klass = get_objects('..che.Che', "relimp.foo.bar")
+        module, klass = get_objects("...baz:Baz", "relimp.foo.bar")
+        self.assertEqual("relimp.baz", module.__name__)
+        self.assertEqual("Baz", klass.__name__)
+
+        module, klass = get_objects("..che.Che", "relimp.foo.bar")
         self.assertEqual("relimp.foo.che", module.__name__)
         self.assertEqual("Che", klass.__name__)
 
-        module, klass = get_objects('...foo.Foo', "relimp.foo.bar")
+        module, klass = get_objects("...foo.Foo", "relimp.foo.bar")
         self.assertEqual("relimp.foo", module.__name__)
         self.assertEqual("Foo", klass.__name__)
 
-        module, klass = get_objects('relimp.Relimp', "relimp.foo.bar")
+        module, klass = get_objects("relimp.Relimp", "relimp.foo.bar")
         self.assertEqual("relimp", module.__name__)
         self.assertEqual("Relimp", klass.__name__)
 
 
 class MakeListTest(TestCase):
     def test_list(self):
-
         a = [
             [1, 2, 3],
             4,
@@ -85,6 +85,6 @@ def test_non_list(self):
         r = make_list("foo")
         self.assertEqual(["foo"], r)
 
-        r = make_list(testdata.get_past_datetime())
+        r = make_list(self.get_past_datetime())
         self.assertEqual(1, len(r))