Repository: RDFLib/rdflib-sqlalchemy (language: python)

Issue:
Call engine.dispose() before setting to None in close()
Hoping this fixes #26 - MySQL connection leak.

See http://docs.sqlalchemy.org/en/latest/core/connections.html#engine-disposal for reference


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index a27c16f..5b6b001 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ rdflib_sqlalchemy.egg-info
 /.eggs/
 /dist/
 /venv/
+docs/api
diff --git a/.travis.yml b/.travis.yml
index 701d605..0553a4a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,32 +1,35 @@
 language: python
+
 env:
-- DB=pgsql
-- DB=mysql
-- DB=sqlite
+ global:
+ - COVERALLS_PARALLEL=true
+ matrix:
+ - DB=pgsql
+ - DB=mysql
+ - DB=sqlite
+
+services:
+- postgresql
+- mysql
+
 python:
 - 2.7
-- 3.3
 - 3.4
 - 3.5
+- 3.6
+- 3.7
+
 install:
-- pip install -U setuptools tox tox-travis
+- pip install -U setuptools tox tox-travis coveralls
+
 before_script:
-- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS test;' -U postgres;
-  psql -c 'create database test;' -U postgres; export DBURI='postgresql+psycopg2://postgres@localhost/test';
-  fi"
-- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'CREATE DATABASE IF NOT EXISTS test';
-  export DBURI='mysql+mysqldb://test@localhost/test?charset=utf8'; fi"
-- sh -c "if [ '$DB' = 'sqlite' ]; then export DBURI='sqlite:///%(here)s/test.sqlite';
-  fi"
+- ./travis-ci.sh
+
 script: tox
-deploy:
-  distributions: sdist bdist_wheel
-  provider: pypi
-  user: adamhadani
-  password:
-    secure: jSAo1pMtzpNSBJHOtn15WiT2XBL6JBTB/1u22CeRhW6tmRh4ngaK1+WAfuoRpgP5seTBnb1DIxiNzvCL5sRjq9ckbHgc7pW9EcpKCeDFjU1MQPH2GqQ37jicc4NY4TXu9pAUIbxIVwGxHWNCY0QPxIgCSVvTTKoiZn3QddL8hzg=
-  on:
-    tags: true
-    repo: RDFLib/rdflib-sqlalchemy
-    branch: develop
-    condition: $TRAVIS_PYTHON_VERSION = "3.5"
+
+notifications:
+  webhooks: https://coveralls.io/webhook
+
+after_script:
+- coveralls
+
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index e87ad52..352ac91 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -8,3 +8,4 @@ Contributors to the rdflib-sqlalchemy project:
 * Yves-Marie Haussonne - https://github.com/ymph 
 * Graham Higgins - https://github.com/gjhiggins
 * Ryan Shaw - https://github.com/rybesh
+* Thibault Clérice - https://github.com/ponteineptique
\ No newline at end of file
diff --git a/README.md b/README.md
index d401a2f..dc781f7 100644
--- a/README.md
+++ b/README.md
@@ -53,40 +53,40 @@ An illustrative unit test:
 
 ```python
 
-    import unittest
-    from rdflib import plugin, Graph, Literal, URIRef
-    from rdflib.store import Store
+import unittest
+from rdflib import plugin, Graph, Literal, URIRef
+from rdflib.store import Store
 
 
-    class SQLASQLiteGraphTestCase(unittest.TestCase):
-        ident = URIRef("rdflib_test")
-        uri = Literal("sqlite://")
+class SQLASQLiteGraphTestCase(unittest.TestCase):
+    ident = URIRef("rdflib_test")
+    uri = Literal("sqlite://")
 
-        def setUp(self):
-            store = plugin.get("SQLAlchemy", Store)(identifier=self.ident)
-            self.graph = Graph(store, identifier=self.ident)
-            self.graph.open(self.uri, create=True)
+    def setUp(self):
+        store = plugin.get("SQLAlchemy", Store)(identifier=self.ident)
+        self.graph = Graph(store, identifier=self.ident)
+        self.graph.open(self.uri, create=True)
 
-        def tearDown(self):
-            self.graph.destroy(self.uri)
-            try:
-                self.graph.close()
-            except:
-                pass
+    def tearDown(self):
+        self.graph.destroy(self.uri)
+        try:
+            self.graph.close()
+        except:
+            pass
 
-        def test01(self):
-            self.assert_(self.graph is not None)
-            print(self.graph)
+    def test01(self):
+        self.assert_(self.graph is not None)
+        print(self.graph)
 
-    if __name__ == '__main__':
-        unittest.main()
+if __name__ == '__main__':
+    unittest.main()
 ```
 
 Running the tests
 =================
-
-This is slightly baroque because of the test matrix (PostgreSQL|MySQL|SQLite
-x Python2.5|2.6|2.7|3.2) ...
+`nose` and `tox` are supported as test runners. Select the SQL back-end by
+setting a `DB` environment variable. Select the database connection by setting
+the `DBURI` variable. With `tox`, you can also specify the Python version.
 
 Using nose::
 
@@ -96,7 +96,8 @@ Using tox::
 
     DB='pgsql' DBURI='postgresql+psycopg2://user:password@host/dbname' tox -e py32
 
-DB variants are 'pgsql', 'mysql' and 'sqlite' 
+DB variants are 'pgsql', 'mysql' and 'sqlite'. Except in the case of SQLite,
+you'll need to create the database independently, before execution of the test.
 
 Sample DBURI values::
 
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..b4dee38
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,281 @@
+# -*- coding: utf-8 -*-
+#
+# rdflib_sqlalchemy documentation build configuration file
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+from sphinx.ext.apidoc import main as apidoc_main, OPTIONS as apidoc_options
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../'))
+
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+autodoc_default_flags = []
+autoclass_content = 'both'
+autodoc_member_order = 'bysource'
+autodoc_typehints = 'none'
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode',
+              'sphinx.ext.intersphinx', 'sphinx.ext.napoleon']
+intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
+                       'rdflib': ('https://rdflib.readthedocs.io/en/stable/', None)}
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'RDFLib SQLAlchemy'
+copyright = u'2013-2019 RDFLib Team'
+
+# For substitutions in docs
+rst_epilog = """
+.. |RDF| replace:: :abbr:`RDF (Resource Description Framework)`
+..
+""" % dict(project=project)
+
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+
+
+# The short X.Y version.
+try:
+    import rdflib_sqlalchemy
+    version = rdflib_sqlalchemy.__version__
+    # The full version, including alpha/beta/rc tags.
+    release = rdflib_sqlalchemy.__version__
+except Exception:
+    version = 'dev'
+    release = 'dev'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+default_role = 'py:obj'
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+todo_include_todos = True
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+#keep_warnings = False
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+
+# on_rtd is whether we are on readthedocs.org
+on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+
+html_theme = 'alabaster'
+
+apidoc_options[:] = ['members', 'no-undoc-members', 'show-inheritance']
+apidoc_main('-M -f -e -d 2 -o api ../rdflib_sqlalchemy'.split(' '))
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {'show_related': True}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+#html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+html_sidebars = {
+    '**': [
+        'relations.html',  # needs 'show_related': True theme option to display
+        'searchbox.html',
+    ]
+}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'rdflib_sqlalchemy_doc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'rdflib_sqlalchemy.tex', u'RDFLib SQLAlchemy Documentation',
+   u'rdflib_sqlalchemy', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'rdflib_sqlalchemy', u'RDFLib SQLAlchemy Documentation',
+     [u'rdflib_sqlalchemy'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'rdflib_sqlalchemy', u'RDFLib Documentation',
+   u'rdflib_sqlalchemy', 'rdflib_sqlalchemy', 'RDFLib SQLAlchemy store',
+   'rdflib_sqlalchemy'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#texinfo_no_detailmenu = False
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..69c37ca
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,27 @@
+.. rdflib_sqlalchemy documentation master file.
+
+Welcome to the documentation for rdflib-sqlalchemy! 
+===================================================
+
+Contents:
+
+.. toctree::
+   :maxdepth: 4
+
+   api/modules
+
+
+Issues
+------
+See our `issue tracker`_ on Github.com
+
+.. _issue tracker: https://github.com/rdflib/rdflib-sqlalchemy/issues
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/rdflib_sqlalchemy/__init__.py b/rdflib_sqlalchemy/__init__.py
index 74773e6..0d6c7d0 100644
--- a/rdflib_sqlalchemy/__init__.py
+++ b/rdflib_sqlalchemy/__init__.py
@@ -44,11 +44,9 @@ def registerplugins():
     from rdflib import plugin
 
     try:
-        x = plugin.get("SQLAlchemy", Store)
-        del x
-        return  # plugins already registered
-    except:
-        pass  # must register plugins
+        plugin.get("SQLAlchemy", Store)
+    except plugin.PluginException:
+        pass
 
     # Register the plugins ...
 
diff --git a/rdflib_sqlalchemy/base.py b/rdflib_sqlalchemy/base.py
index d6c4136..111078f 100644
--- a/rdflib_sqlalchemy/base.py
+++ b/rdflib_sqlalchemy/base.py
@@ -35,6 +35,7 @@ def _build_literal_triple_sql_command(self, subject, predicate, obj, context):
         triple_pattern = int(
             statement_to_term_combination(subject, predicate, obj, context)
         )
+
         command = self.tables["literal_statements"].insert()
         values = {
             "subject": subject,
diff --git a/rdflib_sqlalchemy/constants.py b/rdflib_sqlalchemy/constants.py
index 8c743d3..c0de747 100644
--- a/rdflib_sqlalchemy/constants.py
+++ b/rdflib_sqlalchemy/constants.py
@@ -3,43 +3,60 @@
 
 
 COUNT_SELECT = 0
+''' Return the count of the number of result tuples '''
+
 CONTEXT_SELECT = 1
+''' Return the matching contexts '''
+
 TRIPLE_SELECT = 2
+''' Return the matching triples '''
+
 TRIPLE_SELECT_NO_ORDER = 3
+''' Return the matching triples in any order '''
 
 ASSERTED_NON_TYPE_PARTITION = 3
+''' Table of non-type triples '''
+
 ASSERTED_TYPE_PARTITION = 4
+''' Table of type triples '''
+
 QUOTED_PARTITION = 5
+''' Table of `quoted`_ type triples
+
+.. _quoted: https://rdflib.readthedocs.io/en/stable/univrdfstore.html#terminology
+'''
+
 ASSERTED_LITERAL_PARTITION = 6
+''' Table of triples with literals '''
 
 FULL_TRIPLE_PARTITIONS = [QUOTED_PARTITION, ASSERTED_LITERAL_PARTITION]
 
 INTERNED_PREFIX = "kb_"
 
 TERM_COMBINATIONS = dict([(term, index) for index, term in enumerate([
-     "UUUU", "UUUB", "UUUF", "UUVU", "UUVB", "UUVF", "UUBU", "UUBB", "UUBF",
-     "UULU", "UULB", "UULF", "UUFU", "UUFB", "UUFF",
-     #
-     "UVUU", "UVUB", "UVUF", "UVVU", "UVVB", "UVVF", "UVBU", "UVBB", "UVBF",
-     "UVLU", "UVLB", "UVLF", "UVFU", "UVFB", "UVFF",
-     #
-     "VUUU", "VUUB", "VUUF", "VUVU", "VUVB", "VUVF", "VUBU", "VUBB", "VUBF",
-     "VULU", "VULB", "VULF", "VUFU", "VUFB", "VUFF",
-     #
-     "VVUU", "VVUB", "VVUF", "VVVU", "VVVB", "VVVF", "VVBU", "VVBB", "VVBF",
-     "VVLU", "VVLB", "VVLF", "VVFU", "VVFB", "VVFF",
-     #
-     "BUUU", "BUUB", "BUUF", "BUVU", "BUVB", "BUVF", "BUBU", "BUBB", "BUBF",
-     "BULU", "BULB", "BULF", "BUFU", "BUFB", "BUFF",
-     #
-     "BVUU", "BVUB", "BVUF", "BVVU", "BVVB", "BVVF", "BVBU", "BVBB", "BVBF",
-     "BVLU", "BVLB", "BVLF", "BVFU", "BVFB", "BVFF",
-     #
-     "FUUU", "FUUB", "FUUF", "FUVU", "FUVB", "FUVF", "FUBU", "FUBB", "FUBF",
-     "FULU", "FULB", "FULF", "FUFU", "FUFB", "FUFF",
-     #
-     "FVUU", "FVUB", "FVUF", "FVVU", "FVVB", "FVVF", "FVBU", "FVBB", "FVBF",
-     "FVLU", "FVLB", "FVLF", "FVFU", "FVFB", "FVFF",
+    "UUUU", "UUUB", "UUUF", "UUVU", "UUVB", "UUVF", "UUBU", "UUBB", "UUBF",
+    "UULU", "UULB", "UULF", "UUFU", "UUFB", "UUFF",
+    #
+    "UVUU", "UVUB", "UVUF", "UVVU", "UVVB", "UVVF", "UVBU", "UVBB", "UVBF",
+    "UVLU", "UVLB", "UVLF", "UVFU", "UVFB", "UVFF",
+    #
+    "VUUU", "VUUB", "VUUF", "VUVU", "VUVB", "VUVF", "VUBU", "VUBB", "VUBF",
+    "VULU", "VULB", "VULF", "VUFU", "VUFB", "VUFF",
+    #
+    "VVUU", "VVUB", "VVUF", "VVVU", "VVVB", "VVVF", "VVBU", "VVBB", "VVBF",
+    "VVLU", "VVLB", "VVLF", "VVFU", "VVFB", "VVFF",
+    #
+    "BUUU", "BUUB", "BUUF", "BUVU", "BUVB", "BUVF", "BUBU", "BUBB", "BUBF",
+    "BULU", "BULB", "BULF", "BUFU", "BUFB", "BUFF",
+    #
+    "BVUU", "BVUB", "BVUF", "BVVU", "BVVB", "BVVF", "BVBU", "BVBB", "BVBF",
+    "BVLU", "BVLB", "BVLF", "BVFU", "BVFB", "BVFF",
+    #
+    "FUUU", "FUUB", "FUUF", "FUVU", "FUVB", "FUVF", "FUBU", "FUBB", "FUBF",
+    "FULU", "FULB", "FULF", "FUFU", "FUFB", "FUFF",
+    #
+    "FVUU", "FVUB", "FVUF", "FVVU", "FVVB", "FVVF", "FVBU", "FVBB", "FVBF",
+    "FVLU", "FVLB", "FVLF", "FVFU", "FVFB", "FVFF",
 ])])
 
 REVERSE_TERM_COMBINATIONS = dict([
diff --git a/rdflib_sqlalchemy/sql.py b/rdflib_sqlalchemy/sql.py
index 10b32e6..96ffaf3 100644
--- a/rdflib_sqlalchemy/sql.py
+++ b/rdflib_sqlalchemy/sql.py
@@ -37,29 +37,29 @@ def query_analysis(query, store, connection):
     store.queryOptMarks[(_key, table)] = hits + 1
 
 
-def union_select(selectComponents, distinct=False, select_type=TRIPLE_SELECT):
+def union_select(select_components, distinct=False, select_type=TRIPLE_SELECT):
     """
     Helper function for building union all select statement.
 
-    Terms: u - uri refs  v - variables  b - bnodes l - literal f - formula
+    Args:
+        select_components (iterable of tuples): Indicates the table and table type
+            (table_name, where_clause_string, table_type)
+        distinct (bool): Whether to eliminate duplicate results
+        select_type (int): From `rdflib_sqlalchemy.constants`. Either `COUNT_SELECT`,
+            `CONTEXT_SELECT`, `TRIPLE_SELECT`
 
-    Takes a list of:
-     - table name
-     - table alias
-     - table type (literal, type, asserted, quoted)
-     - where clause string
     """
     selects = []
-    for table, whereClause, tableType in selectComponents:
+    for table, whereClause, tableType in select_components:
 
         if select_type == COUNT_SELECT:
-            selectClause = table.count(whereClause)
+            select_clause = table.count(whereClause)
         elif select_type == CONTEXT_SELECT:
-            selectClause = expression.select([table.c.context], whereClause)
+            select_clause = expression.select([table.c.context], whereClause)
         elif tableType in FULL_TRIPLE_PARTITIONS:
-            selectClause = table.select(whereClause)
+            select_clause = table.select(whereClause)
         elif tableType == ASSERTED_TYPE_PARTITION:
-            selectClause = expression.select(
+            select_clause = expression.select(
                 [table.c.id.label("id"),
                  table.c.member.label("subject"),
                  expression.literal(text_type(RDF.type)).label("predicate"),
@@ -70,14 +70,14 @@ def union_select(selectComponents, distinct=False, select_type=TRIPLE_SELECT):
                  expression.literal_column("NULL").label("objdatatype")],
                 whereClause)
         elif tableType == ASSERTED_NON_TYPE_PARTITION:
-            selectClause = expression.select(
+            select_clause = expression.select(
                 [c for c in table.columns] +
                 [expression.literal_column("NULL").label("objlanguage"),
                  expression.literal_column("NULL").label("objdatatype")],
                 whereClause,
                 from_obj=[table])
 
-        selects.append(selectClause)
+        selects.append(select_clause)
 
     order_statement = []
     if select_type == TRIPLE_SELECT:
@@ -86,7 +86,7 @@ def union_select(selectComponents, distinct=False, select_type=TRIPLE_SELECT):
             expression.literal_column("predicate"),
             expression.literal_column("object"),
         ]
-    if distinct:
+    if distinct and select_type != COUNT_SELECT:
         return expression.union(*selects, **{"order_by": order_statement})
     else:
         return expression.union_all(*selects, **{"order_by": order_statement})
diff --git a/rdflib_sqlalchemy/store.py b/rdflib_sqlalchemy/store.py
index 8e5ddfd..384360e 100644
--- a/rdflib_sqlalchemy/store.py
+++ b/rdflib_sqlalchemy/store.py
@@ -14,9 +14,7 @@
 from rdflib.plugins.stores.regexmatching import PYTHON_REGEX, REGEXTerm
 from rdflib.store import CORRUPTED_STORE, VALID_STORE, NodePickler, Store
 from six import text_type
-from six.moves import reduce
 from sqlalchemy import MetaData
-from sqlalchemy.engine import reflection
 from sqlalchemy.sql import expression, select
 
 from rdflib_sqlalchemy.constants import (
@@ -48,6 +46,25 @@
 Any = None
 
 
+def grouper(iterable, n):
+    "Collect data into chunks of at most n elements"
+    assert n > 0, 'Cannot group into chunks of zero elements'
+    lst = []
+    iterable = iter(iterable)
+    while True:
+        try:
+            lst.append(next(iterable))
+        except StopIteration:
+            break
+
+        if len(lst) == n:
+            yield lst
+            lst = []
+
+    if lst:
+        yield lst
+
+
 def generate_interned_id(identifier):
     return "{prefix}{identifier_hash}".format(
         prefix=INTERNED_PREFIX,
@@ -78,17 +95,26 @@ class SQLAlchemy(Store, SQLGeneratorMixin, StatisticsMixin):
     regex_matching = PYTHON_REGEX
     configuration = Literal("sqlite://")
 
-    def __init__(self, identifier=None, configuration=None, engine=None):
+    def __init__(self, identifier=None, configuration=None, engine=None,
+                 max_terms_per_where=800):
         """
         Initialisation.
 
         Args:
             identifier (rdflib.URIRef): URIRef of the Store. Defaults to CWD.
-            engine (sqlalchemy.engine.Engine, optional): a `SQLAlchemy.engine.Engine` instance
-
+            configuration: the database connection URL string or a configuration dictionary
+                corresponding to the connection options accepted by sqlalchemy.create_engine,
+                with the additional "url" key pointing to the connection URL. See `open` documentation
+                for more details.
+            engine (sqlalchemy.engine.Engine, optional): a pre-existing `SQLAlchemy.engine.Engine` instance.
+            max_terms_per_where (int): The max number of terms (s/p/o) in a call to
+                triples_choices to combine in one SQL "where" clause. Important for SQLite
+                back-end with SQLITE_MAX_EXPR_DEPTH limit and SQLITE_LIMIT_COMPOUND_SELECT
+                -- must find a balance that doesn't hit either of those.
         """
         self.identifier = identifier and identifier or "hardcoded"
         self.engine = engine
+        self.max_terms_per_where = max_terms_per_where
 
         # Use only the first 10 bytes of the digest
         self._interned_id = generate_interned_id(self.identifier)
@@ -116,8 +142,7 @@ def __init__(self, identifier=None, configuration=None, engine=None):
 
         # XXX For backward compatibility we still support getting the connection string in constructor
         # TODO: deprecate this once refactoring is more mature
-        if configuration:
-            self.open(configuration)
+        super(SQLAlchemy, self).__init__(configuration)
 
     def __repr__(self):
         """Readable serialisation."""
@@ -140,13 +165,14 @@ def __repr__(self):
                 typeLen, quotedLen, assertedLen, literalLen = [
                     rtTuple[0] for rtTuple in rt]
             try:
-                return ("<Partitioned SQL N3 Store: %s " +
-                        "contexts, %s classification assertions, " +
-                        "%s quoted statements, %s property/value " +
+                return ("<Partitioned SQL N3 Store: %s "
+                        "contexts, %s classification assertions, "
+                        "%s quoted statements, %s property/value "
                         "assertions, and %s other assertions>" % (
-                            len([ctx for ctx in self.contexts()]),
+                            sum(1 for _ in self.contexts()),
                             typeLen, quotedLen, literalLen, assertedLen))
             except Exception:
+                _logger.exception('Error creating repr')
                 return "<Partitioned SQL N3 Store>"
         else:
             return "<Partitioned unopened SQL N3 Store>"
@@ -192,7 +218,7 @@ def __len__(self, context=None):
         with self.engine.connect() as connection:
             res = connection.execute(q)
             rt = res.fetchall()
-            return reduce(lambda x, y: x + y, [rtTuple[0] for rtTuple in rt])
+            return sum(rtTuple[0] for rtTuple in rt)
 
     @property
     def table_names(self):
@@ -214,9 +240,16 @@ def node_pickler(self):
 
     def open(self, configuration, create=True):
         """
-        Open the store specified by the configuration string.
+        Open the store specified by the configuration parameter.
 
         Args:
+            configuration: if a string, use as the DBAPI URL. If a dictionary, will use as the
+                `**kwargs` for the sqlalchemy.create_engine() call, and will attempt to
+                extract the connection URL from a 'url' key in that dictionary.
+                A valid connection string will be of the format:
+
+                    dialect[+driver]://user:password@host/dbname[?key=value..]
+
             create (bool): If create is True a store will be created if it does not already
                 exist. If create is False and a store does not already exist
                 an exception is raised. An exception is also raised if a store
@@ -224,15 +257,23 @@ def open(self, configuration, create=True):
                 store.
 
         Returns:
-            int: CORRUPTED_STORE (0) if database exists but is empty,
-                 VALID_STORE (1) if database exists and tables are all there,
-                 NO_STORE (-1) if nothing exists
+            int:
+            - CORRUPTED_STORE (0) if database exists but is empty,
+            - VALID_STORE (1) if database exists and tables are all there,
+            - NO_STORE (-1) if nothing exists
 
         """
         # Close any existing engine connection
         self.close()
 
-        self.engine = sqlalchemy.create_engine(configuration)
+        url, kwargs = configuration, {}
+        if isinstance(configuration, dict):
+            url = configuration.pop("url", None)
+            if not url:
+                raise Exception('Configuration dict is missing the required "url" key')
+            kwargs = configuration
+
+        self.engine = sqlalchemy.create_engine(url, **kwargs)
         with self.engine.connect():
             if create:
                 self.create_all()
@@ -251,38 +292,38 @@ def create_all(self):
     def close(self, commit_pending_transaction=False):
         """
         Close the current store engine connection if one is open.
-
         """
+        if self.engine:
+            self.engine.dispose()
         self.engine = None
 
     def destroy(self, configuration):
         """
         Delete all tables and stored data associated with the store.
-
         """
         if self.engine is None:
             self.engine = self.open(configuration, create=False)
 
-        with self.engine.connect() as connection:
-            trans = connection.begin()
+        with self.engine.begin():
             try:
                 self.metadata.drop_all(self.engine)
-                trans.commit()
             except Exception:
                 _logger.exception("unable to drop table.")
-                trans.rollback()
+                raise
 
     # Triple Methods
 
     def add(self, triple, context=None, quoted=False):
         """Add a triple to the store of triples."""
+        super(SQLAlchemy, self).add(triple, context, quoted)
         subject, predicate, obj = triple
         _, statement, params = self._get_build_command(
             (subject, predicate, obj),
             context, quoted,
         )
 
-        with self.engine.connect() as connection:
+        statement = self._add_ignore_on_conflict(statement)
+        with self.engine.begin() as connection:
             try:
                 connection.execute(statement, params)
             except Exception:
@@ -295,7 +336,9 @@ def add(self, triple, context=None, quoted=False):
     def addN(self, quads):
         """Add a list of triples in quads form."""
         commands_dict = {}
+        add_event = super(SQLAlchemy, self).add
         for subject, predicate, obj, context in quads:
+            add_event((subject, predicate, obj), context)
             command_type, statement, params = self._get_build_command(
                 (subject, predicate, obj),
                 context,
@@ -306,19 +349,28 @@ def addN(self, quads):
             command_dict.setdefault("statement", statement)
             command_dict.setdefault("params", []).append(params)
 
-        with self.engine.connect() as connection:
-            trans = connection.begin()
+        with self.engine.begin() as connection:
             try:
                 for command in commands_dict.values():
-                    connection.execute(command["statement"], command["params"])
-                trans.commit()
+                    statement = self._add_ignore_on_conflict(command['statement'])
+                    connection.execute(statement, command["params"])
             except Exception:
                 _logger.exception("AddN failed.")
-                trans.rollback()
                 raise
 
+    def _add_ignore_on_conflict(self, statement):
+        if self.engine.name == 'sqlite':
+            statement = statement.prefix_with('OR IGNORE')
+        elif self.engine.name == 'mysql':
+            statement = statement.prefix_with('IGNORE')
+        elif self.engine.name == 'postgresql':
+            from sqlalchemy.dialects.postgresql.dml import OnConflictDoNothing
+            statement._post_values_clause = OnConflictDoNothing()
+        return statement
+
     def remove(self, triple, context):
         """Remove a triple from the store."""
+        super(SQLAlchemy, self).remove(triple, context)
         subject, predicate, obj = triple
 
         if context is not None:
@@ -331,8 +383,7 @@ def remove(self, triple, context):
         asserted_type_table = self.tables["type_statements"]
         literal_table = self.tables["literal_statements"]
 
-        with self.engine.connect() as connection:
-            trans = connection.begin()
+        with self.engine.begin() as connection:
             try:
                 if not predicate or predicate != RDF.type:
                     # Need to remove predicates other than rdf:type
@@ -359,31 +410,11 @@ def remove(self, triple, context):
 
                     clause = self.build_clause(quoted_table, subject, predicate, obj, context)
                     connection.execute(quoted_table.delete(clause))
-
-                trans.commit()
             except Exception:
                 _logger.exception("Removal failed.")
-                trans.rollback()
-
-    def triples(self, triple, context=None):
-        """
-        A generator over all the triples matching pattern.
-
-        Pattern can be any objects for comparing against nodes in
-        the store, for example, RegExLiteral, Date? DateRange?
-
-        quoted table:                <id>_quoted_statements
-        asserted rdf:type table:     <id>_type_statements
-        asserted non rdf:type table: <id>_asserted_statements
-
-        triple columns:
-            subject, predicate, object, context, termComb, objLanguage, objDatatype
-        class membership columns:
-            member, klass, context, termComb
-
-        FIXME:  These union all selects *may* be further optimized by joins
+                raise
 
-        """
+    def _triples_helper(self, triple, context=None):
         subject, predicate, obj = triple
 
         quoted_table = self.tables["quoted_statements"]
@@ -409,10 +440,10 @@ class membership columns:
             # Literal partition if (obj is Literal or None) and asserted
             # non rdf:type partition (if obj is URIRef or None)
             selects = []
-            if not self.STRONGLY_TYPED_TERMS \
-                    or isinstance(obj, Literal) \
-                    or not obj \
-                    or (self.STRONGLY_TYPED_TERMS and isinstance(obj, REGEXTerm)):
+            if (not self.STRONGLY_TYPED_TERMS
+                    or isinstance(obj, Literal)
+                    or not obj
+                    or (self.STRONGLY_TYPED_TERMS and isinstance(obj, REGEXTerm))):
                 literal = expression.alias(literal_table, "literal")
                 clause = self.build_clause(literal, subject, predicate, obj, context)
                 selects.append((literal, clause, ASSERTED_LITERAL_PARTITION))
@@ -453,7 +484,10 @@ class membership columns:
             clause = self.build_clause(quoted, subject, predicate, obj, context)
             selects.append((quoted, clause, QUOTED_PARTITION))
 
-        q = union_select(selects, select_type=TRIPLE_SELECT_NO_ORDER)
+        return selects
+
+    def _do_triples_select(self, selects, context):
+        q = union_select(selects, distinct=True, select_type=TRIPLE_SELECT_NO_ORDER)
         with self.engine.connect() as connection:
             res = connection.execute(q)
             # TODO: False but it may have limitations on text column. Check
@@ -472,17 +506,19 @@ class membership columns:
         for (s, p, o), contexts in tripleCoverage.items():
             yield (s, p, o), (c for c in contexts)
 
+    def triples(self, triple, context=None):
+        """ A generator over all the triples matching a pattern. """
+        selects = self._triples_helper(triple, context)
+        for m in self._do_triples_select(selects, context):
+            yield m
+
     def triples_choices(self, triple, context=None):
         """
         A variant of triples.
-
-        Can take a list of terms instead of a single term in any slot.
-        Stores can implement this to optimize the response time from the
-        import default 'fallback' implementation, which will iterate over
-        each term in the list and dispatch to triples.
         """
+        # We already support accepting a list for s/p/o
         subject, predicate, object_ = triple
-
+        selects = []
         if isinstance(object_, list):
             assert not isinstance(
                 subject, list), "object_ / subject are both lists"
@@ -490,27 +526,30 @@ def triples_choices(self, triple, context=None):
                 predicate, list), "object_ / predicate are both lists"
             if not object_:
                 object_ = None
-            for (s1, p1, o1), cg in self.triples(
-                    (subject, predicate, object_), context):
-                yield (s1, p1, o1), cg
+            for o in grouper(object_, self.max_terms_per_where):
+                for sels in self._triples_helper((subject, predicate, o), context):
+                    selects.append(sels)
 
         elif isinstance(subject, list):
             assert not isinstance(
                 predicate, list), "subject / predicate are both lists"
             if not subject:
                 subject = None
-            for (s1, p1, o1), cg in self.triples(
-                    (subject, predicate, object_), context):
-                yield (s1, p1, o1), cg
+            for s in grouper(subject, self.max_terms_per_where):
+                for sels in self._triples_helper((s, predicate, object_), context):
+                    selects.append(sels)
 
         elif isinstance(predicate, list):
             assert not isinstance(
                 subject, list), "predicate / subject are both lists"
             if not predicate:
                 predicate = None
-            for (s1, p1, o1), cg in self.triples(
-                    (subject, predicate, object_), context):
-                yield (s1, p1, o1), cg
+            for p in grouper(predicate, self.max_terms_per_where):
+                for sels in self._triples_helper((subject, p, object_), context):
+                    selects.append(sels)
+
+        for m in self._do_triples_select(selects, context):
+            yield m
 
     def contexts(self, triple=None):
         quoted_table = self.tables["quoted_statements"]
@@ -560,9 +599,8 @@ def contexts(self, triple=None):
                 # partition (optionally)
                 selects = []
                 if (not self.STRONGLY_TYPED_TERMS or
-                        isinstance(obj, Literal) or
-                        not obj
-                        or (self.STRONGLY_TYPED_TERMS and isinstance(obj, REGEXTerm))):
+                        isinstance(obj, Literal) or not obj or (
+                            self.STRONGLY_TYPED_TERMS and isinstance(obj, REGEXTerm))):
                     clause = self.build_clause(literal, subject, predicate, obj)
                     selects.append(
                         (literal, clause, ASSERTED_LITERAL_PARTITION))
@@ -590,65 +628,20 @@ def contexts(self, triple=None):
         for context in [rtTuple[0] for rtTuple in rt]:
             yield URIRef(context)
 
-    # Optional Namespace methods
-
-    # Placeholder optimized interfaces (those needed in order to port Versa)
-    def subjects(self, predicate=None, obj=None):
-        """A generator of subjects with the given predicate and object."""
-        raise Exception("Not implemented")
-
-    # Capable of taking a list of predicate terms instead of a single term
-    def objects(self, subject=None, predicate=None):
-        """A generator of objects with the given subject and predicate."""
-        raise Exception("Not implemented")
-
-    # Optimized interfaces (others)
-    def predicate_objects(self, subject=None):
-        """A generator of (predicate, object) tuples for the given subject."""
-        raise Exception("Not implemented")
-
-    def subject_objects(self, predicate=None):
-        """A generator of (subject, object) tuples for the given predicate."""
-        raise Exception("Not implemented")
-
-    def subject_predicates(self, object=None):
-        """A generator of (subject, predicate) tuples for the given object."""
-        raise Exception("Not implemented")
-
-    def value(self, subject,
-              predicate=u"http://www.w3.org/1999/02/22-rdf-syntax-ns#value",
-              object=None, default=None, any=False):
-        """
-        Get a value.
-
-        For a subject/predicate, predicate/object, or
-        subject/object pair -- exactly one of subject, predicate,
-        object must be None. Useful if one knows that there may only
-        be one value.
-
-        It is one of those situations that occur a lot, hence this
-        'macro' like utility
-
-        :param subject:
-        :param predicate:
-        :param object:  -- exactly one must be None
-        :param default: -- value to be returned if no values found
-        :param any: -- if true, return any value in the case there is more
-                       than one, else raise a UniquenessError
-        """
-        raise Exception("Not implemented")
-
     # Namespace persistence interface implementation
 
     def bind(self, prefix, namespace):
         """Bind prefix for namespace."""
-        with self.engine.connect() as connection:
+        with self.engine.begin() as connection:
             try:
-                ins = self.tables["namespace_binds"].insert().values(
-                    prefix=prefix, uri=namespace)
-                connection.execute(ins)
+                binds_table = self.tables["namespace_binds"]
+                prefix = text_type(prefix)
+                namespace = text_type(namespace)
+                connection.execute(binds_table.delete().where(binds_table.c.prefix == prefix))
+                connection.execute(binds_table.insert().values(prefix=prefix, uri=namespace))
             except Exception:
                 _logger.exception("Namespace binding failed.")
+                raise
 
     def prefix(self, namespace):
         """Prefix."""
@@ -659,7 +652,9 @@ def prefix(self, namespace):
             res = connection.execute(s)
             rt = [rtTuple[0] for rtTuple in res.fetchall()]
             res.close()
-            return rt and rt[0] or None
+            if rt and (rt[0] or rt[0] == ""):
+                return rt[0]
+        return None
 
     def namespace(self, prefix):
         res = None
@@ -672,12 +667,13 @@ def namespace(self, prefix):
                 rt = [rtTuple[0] for rtTuple in res.fetchall()]
                 res.close()
                 return rt and URIRef(rt[0]) or None
-        except:
+        except Exception:
+            _logger.warning('exception in namespace retrieval', exc_info=True)
             return None
 
     def namespaces(self):
         with self.engine.connect() as connection:
-            res = connection.execute(self.tables["namespace_binds"].select())
+            res = connection.execute(self.tables["namespace_binds"].select(distinct=True))
             for prefix, uri in res.fetchall():
                 yield prefix, uri
 
@@ -739,37 +735,32 @@ def _get_build_command(self, triple, context=None, quoted=False):
             command_type = "type"
         return command_type, statement, params
 
-    def _remove_context(self, identifier):
+    def _remove_context(self, context):
         """Remove context."""
-        assert identifier
+        assert context
         quoted_table = self.tables["quoted_statements"]
         asserted_table = self.tables["asserted_statements"]
         asserted_type_table = self.tables["type_statements"]
         literal_table = self.tables["literal_statements"]
 
-        with self.engine.connect() as connection:
-            trans = connection.begin()
+        with self.engine.begin() as connection:
             try:
                 for table in [quoted_table, asserted_table,
                               asserted_type_table, literal_table]:
-                    clause = self.build_context_clause(identifier, table)
+                    clause = self.build_context_clause(context, table)
                     connection.execute(table.delete(clause))
-                trans.commit()
             except Exception:
                 _logger.exception("Context removal failed.")
-                trans.rollback()
+                raise
 
     def _verify_store_exists(self):
         """
         Verify store (e.g. all tables) exist.
-
         """
 
-        inspector = reflection.Inspector.from_engine(self.engine)
-        existing_table_names = inspector.get_table_names()
         for table_name in self.table_names:
-            if table_name not in existing_table_names:
-                _logger.critical("create_all() - table %s Doesn't exist!", table_name)
+            if not self.engine.has_table(table_name):
+                _logger.critical("create_all() - table %s is not known", table_name)
                 # The database exists, but one of the tables doesn't exist
                 return CORRUPTED_STORE
 
diff --git a/rdflib_sqlalchemy/termutils.py b/rdflib_sqlalchemy/termutils.py
index 18f1a7b..88c215c 100644
--- a/rdflib_sqlalchemy/termutils.py
+++ b/rdflib_sqlalchemy/termutils.py
@@ -1,7 +1,6 @@
 """Convenience functions for working with Terms and Graphs."""
 from rdflib import BNode, Graph, Literal, URIRef, Variable
 from rdflib.graph import QuotedGraph
-from rdflib.py3compat import format_doctest_out
 from rdflib.term import Statement
 
 from rdflib_sqlalchemy.constants import (
@@ -10,7 +9,6 @@
     REVERSE_TERM_COMBINATIONS,
 )
 
-
 __all__ = ["extract_triple"]
 
 
@@ -26,7 +24,6 @@
 }
 
 
-@format_doctest_out
 def normalize_graph(graph):
     """
     Take an instance of a ``Graph`` and return the instance's identifier and  ``type``.
@@ -43,13 +40,13 @@ def normalize_graph(graph):
     >>> memstore = plugin.get('IOMemory', Store)()
     >>> g = Graph(memstore, URIRef("http://purl.org/net/bel-epa/gjh"))
     >>> normalize_graph(g)
-    (rdflib.term.URIRef(%(u)s'http://purl.org/net/bel-epa/gjh'), 'U')
+    (rdflib.term.URIRef(u'http://purl.org/net/bel-epa/gjh'), 'U')
     >>> g = ConjunctiveGraph(memstore, Namespace("http://rdflib.net/ns"))
     >>> normalize_graph(g)  #doctest: +ELLIPSIS
-    (rdflib.term.URIRef(%(u)s'http://rdflib.net/ns'), 'U')
+    (rdflib.term.URIRef(u'http://rdflib.net/ns'), 'U')
     >>> g = QuotedGraph(memstore, Namespace("http://rdflib.net/ns"))
     >>> normalize_graph(g)
-    (rdflib.term.URIRef(%(u)s'http://rdflib.net/ns'), 'F')
+    (rdflib.term.URIRef(u'http://rdflib.net/ns'), 'F')
 
     """
     if isinstance(graph, QuotedGraph):
@@ -58,7 +55,6 @@ def normalize_graph(graph):
         return graph.identifier, term_to_letter(graph.identifier)
 
 
-@format_doctest_out
 def term_to_letter(term):
     """
     Relate a given term to one of several key types.
@@ -80,9 +76,9 @@ def term_to_letter(term):
     'U'
     >>> term_to_letter(BNode())
     'B'
-    >>> term_to_letter(Literal(%(u)s''))  # noqa
+    >>> term_to_letter(Literal(u''))  # noqa
     'L'
-    >>> term_to_letter(Variable(%(u)s'x'))  # noqa
+    >>> term_to_letter(Variable(u'x'))  # noqa
     'V'
     >>> term_to_letter(Graph())
     'B'
@@ -148,16 +144,10 @@ def triple_pattern_to_term_combinations(triple):
 
 def type_to_term_combination(member, klass, context):
     """Map a type to a term combination."""
-    try:
-        rt = TERM_COMBINATIONS["%sU%s%s" %
-                               (term_to_letter(member),
-                                term_to_letter(klass),
-                                normalize_graph(context)[-1])]
-        return rt
-    except:
-        raise Exception("Unable to persist" +
-                        "classification triple: %s %s %s %s" %
-                        (member, "rdf:type", klass, context))
+    return TERM_COMBINATIONS["%sU%s%s" %
+                             (term_to_letter(member),
+                              term_to_letter(klass),
+                              normalize_graph(context)[-1])]
 
 
 def statement_to_term_combination(subject, predicate, obj, context):
diff --git a/setup.cfg b/setup.cfg
index efa7e9e..9776981 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,7 +4,9 @@ description-file = README.md
 [nosetests]
 attr = !known_issue,!performancetest
 verbosity = 2
+where = test
 
 [flake8]
 max-line-length = 120
 max-complexity = 15
+ignore = E722
diff --git a/setup.py b/setup.py
index 8a3acf0..04ce11f 100644
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,12 @@
 #!/usr/bin/env python
+import sys
+
 from setuptools import setup
 
+PY2 = sys.version_info.major == 2
 
 project = "rdflib-sqlalchemy"
-version = "0.3.8"
+version = "0.4.0"
 
 
 setup(
@@ -39,6 +42,8 @@
         "Programming Language :: Python :: 3.3",
         "Programming Language :: Python :: 3.4",
         "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
         "License :: OSI Approved :: BSD License",
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Operating System :: OS Independent",
@@ -53,6 +58,13 @@
     setup_requires=[
         "nose>=1.3.6",
     ],
-    tests_require="coveralls",
+    tests_require=[
+        "coveralls"
+    ] + (['mock'] if PY2 else []),
     test_suite="nose.collector",
+    entry_points={
+        'rdf.plugins.store': [
+            'SQLAlchemy = rdflib_sqlalchemy.store:SQLAlchemy'
+        ]
+    }
 )
diff --git a/tox.ini b/tox.ini
index 97fca13..d5fb665 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,24 +1,28 @@
 [tox]
 envlist =
-    py27,py34,py35,cover,lint
+    py27,py34,py35,py36,py37,lint
 
 [testenv]
 passenv = DB DBURI
 commands =
     {envpython} setup.py clean --all
-    {envpython} setup.py nosetests
+    {envpython} setup.py nosetests \
+                --with-coverage --cover-package=rdflib_sqlalchemy \
+                --cover-inclusive --cover-branches
+
 deps =
     psycopg2
     mysqlclient
+    py{34,35,36,37}: mysql-connector
 
 [testenv:lint]
 commands = flake8 rdflib_sqlalchemy test
-deps=
+deps =
     flake8
     flake8-print
 
 [testenv:cover]
-basepython = python2.7
+basepython = python3.7
 commands =
     {envpython} setup.py nosetests \
                  --with-coverage --cover-html --cover-html-dir=./coverage \
@@ -27,6 +31,12 @@ deps =
     psycopg2
     mysqlclient
 
-[tox:travis]
-2.7 = py27, cover, lint
-3.5 = py35, lint
+[travis]
+python =
+    2.7: py27, lint
+    3.7: py37, lint
+
+[flake8]
+max-line-length = 120
+max-complexity = 15
+ignore = W504,W503
diff --git a/travis-ci.sh b/travis-ci.sh
new file mode 100755
index 0000000..7740885
--- /dev/null
+++ b/travis-ci.sh
@@ -0,0 +1,20 @@
+#!/bin/bash -ex
+
+if [ "$DB" = 'pgsql' ]; then
+    psql -c 'DROP DATABASE IF EXISTS test;' -U postgres
+    psql -c 'create database test;' -U postgres
+    export DBURI='postgresql+psycopg2://postgres@localhost/test'
+fi
+
+if [ "$DB" = 'mysql' ]; then
+    mysql -e 'CREATE SCHEMA test DEFAULT CHARACTER SET utf8;'
+    if [[ "$TRAVIS_PYTHON_VERSION" =~ '.*3\.[4567].*' ]] ; then
+        export DBURI='mysql+mysqlconnector://test@localhost/test?charset=utf8'
+    else
+        export DBURI='mysql+mysqldb://test@localhost/test?charset=utf8'
+    fi
+fi
+
+if [ "$DB" = 'sqlite' ]; then
+    export DBURI='sqlite:///%(here)s/test.sqlite'
+fi


Test patch:
diff --git a/test/context_case.py b/test/context_case.py
index 1a30ec3..6a90da9 100644
--- a/test/context_case.py
+++ b/test/context_case.py
@@ -30,10 +30,7 @@ def setUp(self, uri="sqlite://", storename=None):
 
     def tearDown(self, uri="sqlite://"):
         self.graph.destroy(uri)
-        try:
-            self.graph.close()
-        except:
-            pass
+        self.graph.close()
 
     def get_context(self, identifier):
         assert isinstance(identifier, URIRef) or \
@@ -99,7 +96,7 @@ def testConjunction(self):
         # add to context 1
         graph = Graph(self.graph.store, self.c1)
         graph.add(triple)
-        self.assertEquals(len(self.graph.store), len(graph.store))
+        self.assertEqual(len(self.graph.store), len(graph.store))
 
     def testAdd(self):
         self.addStuff()
@@ -119,23 +116,23 @@ def testLenInOneContext(self):
 
         for i in range(0, 10):
             graph.add((BNode(), self.hates, self.hates))
-        self.assertEquals(len(graph), oldLen + 10)
-        self.assertEquals(len(self.get_context(c1)), oldLen + 10)
+        self.assertEqual(len(graph), oldLen + 10)
+        self.assertEqual(len(self.get_context(c1)), oldLen + 10)
         self.graph.remove_context(self.get_context(c1))
-        self.assertEquals(len(self.graph), oldLen)
-        self.assertEquals(len(graph), 0)
+        self.assertEqual(len(self.graph), oldLen)
+        self.assertEqual(len(graph), 0)
 
     def testLenInMultipleContexts(self):
         oldLen = len(self.graph.store)
         self.addStuffInMultipleContexts()
         # addStuffInMultipleContexts is adding the same triple to
         # three different contexts. So it's only + 1
-        self.assertEquals(len(self.graph.store), oldLen + 1,
-                          [self.graph.store, oldLen + 1])
+        self.assertEqual(len(self.graph.store), oldLen + 1,
+                         [self.graph.store, oldLen + 1])
 
         graph = Graph(self.graph.store, self.c1)
-        self.assertEquals(len(graph.store), oldLen + 1,
-                          [graph.store, oldLen + 1])
+        self.assertEqual(len(graph.store), oldLen + 1,
+                         [graph.store, oldLen + 1])
 
     def testRemoveInMultipleContexts(self):
         c1 = self.c1
@@ -145,21 +142,21 @@ def testRemoveInMultipleContexts(self):
         self.addStuffInMultipleContexts()
 
         # triple should be still in store after removing it from c1 + c2
-        self.assert_(triple in self.graph)
+        self.assertIn(triple, self.graph)
         graph = Graph(self.graph.store, c1)
         graph.remove(triple)
-        self.assert_(triple in self.graph)
+        self.assertIn(triple, self.graph)
         graph = Graph(self.graph.store, c2)
         graph.remove(triple)
-        self.assert_(triple in self.graph)
+        self.assertIn(triple, self.graph)
         self.graph.remove(triple)
         # now gone!
-        self.assert_(triple not in self.graph)
+        self.assertNotIn(triple, self.graph)
 
         # add again and see if remove without context removes all triples!
         self.addStuffInMultipleContexts()
         self.graph.remove(triple)
-        self.assert_(triple not in self.graph)
+        self.assertNotIn(triple, self.graph)
 
     def testContexts(self):
         triple = (self.pizza, self.hates, self.tarek)  # revenge!
@@ -170,28 +167,28 @@ def cid(c):
             if not isinstance(c, string_types):
                 return c.identifier
             return c
-        self.assert_(self.c1 in list(map(cid, self.graph.contexts())))
-        self.assert_(self.c2 in list(map(cid, self.graph.contexts())))
+        self.assertIn(self.c1, list(map(cid, self.graph.contexts())))
+        self.assertIn(self.c2, list(map(cid, self.graph.contexts())))
 
         contextList = list(map(cid, list(self.graph.contexts(triple))))
-        self.assert_(self.c1 in contextList)
-        self.assert_(self.c2 in contextList)
+        self.assertIn(self.c1, contextList)
+        self.assertIn(self.c2, contextList)
 
     def testRemoveContext(self):
         c1 = self.c1
 
         self.addStuffInMultipleContexts()
-        self.assertEquals(len(Graph(self.graph.store, c1)), 1)
-        self.assertEquals(len(self.get_context(c1)), 1)
+        self.assertEqual(len(Graph(self.graph.store, c1)), 1)
+        self.assertEqual(len(self.get_context(c1)), 1)
 
         self.graph.remove_context(self.get_context(c1))
-        self.assert_(self.c1 not in self.graph.contexts())
+        self.assertNotIn(self.c1, self.graph.contexts())
 
     def testRemoveAny(self):
         Any = None
         self.addStuffInMultipleContexts()
         self.graph.remove((Any, Any, Any))
-        self.assertEquals(len(self.graph), 0)
+        self.assertEqual(len(self.graph), 0)
 
     def testTriples(self):
         tarek = self.tarek
@@ -202,7 +199,7 @@ def testTriples(self):
         pizza = self.pizza
         cheese = self.cheese
         c1 = self.c1
-        asserte = self.assertEquals
+        asserte = self.assertEqual
         triples = self.graph.triples
         graph = self.graph
         c1graph = Graph(self.graph.store, c1)
diff --git a/test/graph_case.py b/test/graph_case.py
index 4b1c4d9..84bc18f 100644
--- a/test/graph_case.py
+++ b/test/graph_case.py
@@ -1,12 +1,9 @@
 # -*- coding: utf-8 -*-
 import unittest
 
-from rdflib import Graph
-from rdflib import URIRef
-from rdflib import Literal
-from rdflib import plugin
+from rdflib import Graph, URIRef, Literal, plugin, RDF
 from rdflib.parser import StringInputSource
-from rdflib.py3compat import PY3
+from six import PY3
 from rdflib.store import Store
 
 
@@ -22,6 +19,10 @@ class GraphTestCase(unittest.TestCase):
     pizza = URIRef(u"pizza")
     cheese = URIRef(u"cheese")
 
+    namespace_dc = "http://purl.org/dc/elements/1.1/"
+    namespace_dct = "http://purl.org/dc/terms/"
+    namespace_saws = "http://purl.org/saws/ontology#"
+
     def setUp(self, uri="sqlite://", storename=None):
         store = plugin.get(storename, Store)(identifier=self.identifier)
         self.graph = Graph(store, identifier=self.identifier)
@@ -29,10 +30,7 @@ def setUp(self, uri="sqlite://", storename=None):
 
     def tearDown(self, uri="sqlite://"):
         self.graph.destroy(uri)
-        try:
-            self.graph.close()
-        except:
-            pass
+        self.graph.close()
 
     def addStuff(self):
         tarek = self.tarek
@@ -84,7 +82,7 @@ def testTriples(self):
         hates = self.hates
         pizza = self.pizza
         cheese = self.cheese
-        asserte = self.assertEquals
+        asserte = self.assertEqual
         triples = self.graph.triples
         Any = None
 
@@ -130,14 +128,14 @@ def testTriples(self):
     def testConnected(self):
         graph = self.graph
         self.addStuff()
-        self.assertEquals(True, graph.connected())
+        self.assertEqual(True, graph.connected())
 
         jeroen = URIRef("jeroen")
         unconnected = URIRef("unconnected")
 
         graph.add((jeroen, self.likes, unconnected))
 
-        self.assertEquals(False, graph.connected())
+        self.assertEqual(False, graph.connected())
 
     def testSub(self):
         g1 = Graph()
@@ -158,19 +156,19 @@ def testSub(self):
 
         g3 = g1 - g2
 
-        self.assertEquals(len(g3), 1)
-        self.assertEquals((tarek, likes, pizza) in g3, True)
-        self.assertEquals((tarek, likes, cheese) in g3, False)
+        self.assertEqual(len(g3), 1)
+        self.assertEqual((tarek, likes, pizza) in g3, True)
+        self.assertEqual((tarek, likes, cheese) in g3, False)
 
-        self.assertEquals((bob, likes, cheese) in g3, False)
+        self.assertEqual((bob, likes, cheese) in g3, False)
 
         g1 -= g2
 
-        self.assertEquals(len(g1), 1)
-        self.assertEquals((tarek, likes, pizza) in g1, True)
-        self.assertEquals((tarek, likes, cheese) in g1, False)
+        self.assertEqual(len(g1), 1)
+        self.assertEqual((tarek, likes, pizza) in g1, True)
+        self.assertEqual((tarek, likes, cheese) in g1, False)
 
-        self.assertEquals((bob, likes, cheese) in g1, False)
+        self.assertEqual((bob, likes, cheese) in g1, False)
 
     def testGraphAdd(self):
         g1 = Graph()
@@ -190,19 +188,19 @@ def testGraphAdd(self):
 
         g3 = g1 + g2
 
-        self.assertEquals(len(g3), 2)
-        self.assertEquals((tarek, likes, pizza) in g3, True)
-        self.assertEquals((tarek, likes, cheese) in g3, False)
+        self.assertEqual(len(g3), 2)
+        self.assertEqual((tarek, likes, pizza) in g3, True)
+        self.assertEqual((tarek, likes, cheese) in g3, False)
 
-        self.assertEquals((bob, likes, cheese) in g3, True)
+        self.assertEqual((bob, likes, cheese) in g3, True)
 
         g1 += g2
 
-        self.assertEquals(len(g1), 2)
-        self.assertEquals((tarek, likes, pizza) in g1, True)
-        self.assertEquals((tarek, likes, cheese) in g1, False)
+        self.assertEqual(len(g1), 2)
+        self.assertEqual((tarek, likes, pizza) in g1, True)
+        self.assertEqual((tarek, likes, cheese) in g1, False)
 
-        self.assertEquals((bob, likes, cheese) in g1, True)
+        self.assertEqual((bob, likes, cheese) in g1, True)
 
     def testGraphIntersection(self):
         g1 = Graph()
@@ -224,24 +222,24 @@ def testGraphIntersection(self):
 
         g3 = g1 * g2
 
-        self.assertEquals(len(g3), 1)
-        self.assertEquals((tarek, likes, pizza) in g3, False)
-        self.assertEquals((tarek, likes, cheese) in g3, False)
+        self.assertEqual(len(g3), 1)
+        self.assertEqual((tarek, likes, pizza) in g3, False)
+        self.assertEqual((tarek, likes, cheese) in g3, False)
 
-        self.assertEquals((bob, likes, cheese) in g3, False)
+        self.assertEqual((bob, likes, cheese) in g3, False)
 
-        self.assertEquals((michel, likes, cheese) in g3, True)
+        self.assertEqual((michel, likes, cheese) in g3, True)
 
         g1 *= g2
 
-        self.assertEquals(len(g1), 1)
+        self.assertEqual(len(g1), 1)
 
-        self.assertEquals((tarek, likes, pizza) in g1, False)
-        self.assertEquals((tarek, likes, cheese) in g1, False)
+        self.assertEqual((tarek, likes, pizza) in g1, False)
+        self.assertEqual((tarek, likes, cheese) in g1, False)
 
-        self.assertEquals((bob, likes, cheese) in g1, False)
+        self.assertEqual((bob, likes, cheese) in g1, False)
 
-        self.assertEquals((michel, likes, cheese) in g1, True)
+        self.assertEqual((michel, likes, cheese) in g1, True)
 
     def testStoreLiterals(self):
         bob = self.bob
@@ -258,14 +256,14 @@ def testStoreLiterals(self):
         objs = list(self.graph.objects(subject=bob, predicate=says))
         for o in objs:
             if o.value == u"hello":
-                self.assertEquals(o.language, "en")
+                self.assertEqual(o.language, "en")
             elif o.value == u"こんにちは":
-                self.assertEquals(o.language, "ja")
+                self.assertEqual(o.language, "ja")
             elif o.value == u"something":
-                self.assertEquals(o.language, None)
+                self.assertEqual(o.language, None)
             else:
                 self.fail()
-        self.assertEquals(len(list(objs)), 3)
+        self.assertEqual(len(list(objs)), 3)
 
     def testStoreLiteralsXml(self):
         bob = self.bob
@@ -280,11 +278,11 @@ def testStoreLiteralsXml(self):
         self.graph.parse(StringInputSource(testdoc), formal="xml")
 
         objs = list(self.graph)
-        self.assertEquals(len(objs), 3)
+        self.assertEqual(len(objs), 3)
         for o in objs:
-            self.assertEquals(o[0], bob)
-            self.assertEquals(o[1], says)
-            self.assertTrue(o[2] in objects)
+            self.assertEqual(o[0], bob)
+            self.assertEqual(o[1], says)
+            self.assertIn(o[2], objects)
 
     def testStoreLiteralsXmlQuote(self):
         bob = self.bob
@@ -296,9 +294,102 @@ def testStoreLiteralsXmlQuote(self):
         self.graph.parse(StringInputSource(testdoc), formal="xml")
 
         objs = list(self.graph)
-        self.assertEquals(len(objs), 1)
+        self.assertEqual(len(objs), 1)
         o = objs[0]
-        self.assertEquals(o, (bob, says, imtheone))
+        self.assertEqual(o, (bob, says, imtheone))
+
+    def testBindNamespace(self):
+        """ Check that bound namespaced with prefix (including empty ones) are correctly kept """
+        self.graph.bind("", self.namespace_dc)
+        self.graph.bind("dct", self.namespace_dct)
+        self.assertEqual(
+            self.graph.qname(self.namespace_dct + u"title"), "dct:title",
+            "Prefixed namespace should be stored and retrieved"
+        )
+        self.assertEqual(
+            self.graph.qname(self.namespace_dc + u"title"), "title",
+            "Empty prefixes for namespace should be stored and retrieved"
+        )
+        self.assertEqual(
+            self.graph.qname(self.namespace_saws + u"title"), "ns1:title",
+            "Unknown prefixes for namespace should be transformed to nsX"
+        )
+
+    def testTriplesChoices(self):
+        likes = self.likes
+        pizza = self.pizza
+        cheese = self.cheese
+        tarek = self.tarek
+        michel = self.michel
+        bob = self.bob
+        self.addStuff()
+        trips = self.graph.triples_choices((None, likes, [pizza, cheese]))
+        self.assertEqual(
+            set(trips),
+            set([(tarek, likes, pizza),
+                 (tarek, likes, pizza),
+                 (tarek, likes, cheese),
+                 (michel, likes, pizza),
+                 (michel, likes, cheese),
+                 (bob, likes, cheese)])
+        )
+
+    def test_type_add(self):
+        trip = (URIRef('http://example.org#type-add'), RDF.type, URIRef('http://example.org/cra'))
+        self.graph.add(trip)
+        self.graph.add(trip)
+        # No exception raised
+
+    def test_type_addn(self):
+        quad = (URIRef('http://example.org#type-addn'), RDF.type, URIRef('http://example.org/cra'), self.graph)
+        self.graph.addN([quad, quad])
+        # No exception raised
+
+    def test_add(self):
+        trip = (URIRef('http://example.org#add'), URIRef('http://example.org/blah'), URIRef('http://example.org/cra'))
+        self.graph.add(trip)
+        self.graph.add(trip)
+        # No exception raised
+
+    def test_addn(self):
+        quad = (URIRef('http://example.org#addn'),
+                URIRef('http://example.org/blah'),
+                URIRef('http://example.org/cra'),
+                self.graph)
+        self.graph.addN([quad, quad])
+        # No exception raised
+
+    def test_namespace_change_prefix_binding(self):
+        nm = self.graph.namespace_manager
+        nm.bind('change_binding', URIRef('http://example.org/change-binding-1#'),
+                replace=True)
+        nm.bind('change_binding', URIRef('http://example.org/change-binding-2#'),
+                replace=True)
+        self.assertIn(
+            ('change_binding', URIRef('http://example.org/change-binding-2#')),
+            list(nm.namespaces())
+        )
+
+    def test_namespace_rebind_prefix(self):
+        nm = self.graph.namespace_manager
+        nm.bind('rebind', URIRef('http://example.org/rebind#'))
+        nm.bind('rebind', URIRef('http://example.org/rebind#'))
+
+    def test_add_duplicate_length(self):
+        '''
+        Test that adding duplicate triples doesn't increase the length of the graph
+        '''
+        trip = (URIRef('http://example.org#add'), URIRef('http://example.org/blah'), URIRef('http://example.org/cra'))
+        self.graph.add(trip)
+        self.graph.add(trip)
+        self.assertEqual(len(self.graph), 1)
+    # additional tests
+    # - add "duplicate" triples and query -- ensure the graph length counts only distinct
+    #   triples
+    # - add duplicate triples and query -- ensure there are no duplicate entries in the
+    #   query result
+    # - repeat the above two for type triples
+    # - test with quoted graphs (not even sure how that works)
 
 
 xmltestdoc = """<?xml version="1.0" encoding="UTF-8"?>
diff --git a/test/rdf-schema.n3 b/test/rdf-schema.n3
new file mode 100644
index 0000000..a5b8eac
--- /dev/null
+++ b/test/rdf-schema.n3
@@ -0,0 +1,109 @@
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix dc: <http://purl.org/dc/elements/1.1/> .
+
+<http://www.w3.org/2000/01/rdf-schema#> a owl:Ontology ;
+	dc:title "The RDF Schema vocabulary (RDFS)" .
+
+rdfs:Resource a rdfs:Class ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "Resource" ;
+	rdfs:comment "The class resource, everything." .
+
+rdfs:Class a rdfs:Class ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "Class" ;
+	rdfs:comment "The class of classes." ;
+	rdfs:subClassOf rdfs:Resource .
+
+rdfs:subClassOf a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "subClassOf" ;
+	rdfs:comment "The subject is a subclass of a class." ;
+	rdfs:range rdfs:Class ;
+	rdfs:domain rdfs:Class .
+
+rdfs:subPropertyOf a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "subPropertyOf" ;
+	rdfs:comment "The subject is a subproperty of a property." ;
+	rdfs:range rdf:Property ;
+	rdfs:domain rdf:Property .
+
+rdfs:comment a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "comment" ;
+	rdfs:comment "A description of the subject resource." ;
+	rdfs:domain rdfs:Resource ;
+	rdfs:range rdfs:Literal .
+
+rdfs:label a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "label" ;
+	rdfs:comment "A human-readable name for the subject." ;
+	rdfs:domain rdfs:Resource ;
+	rdfs:range rdfs:Literal .
+
+rdfs:domain a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "domain" ;
+	rdfs:comment "A domain of the subject property." ;
+	rdfs:range rdfs:Class ;
+	rdfs:domain rdf:Property .
+
+rdfs:range a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "range" ;
+	rdfs:comment "A range of the subject property." ;
+	rdfs:range rdfs:Class ;
+	rdfs:domain rdf:Property .
+
+rdfs:seeAlso a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "seeAlso" ;
+	rdfs:comment "Further information about the subject resource." ;
+	rdfs:range rdfs:Resource ;
+	rdfs:domain rdfs:Resource .
+
+rdfs:isDefinedBy a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:subPropertyOf rdfs:seeAlso ;
+	rdfs:label "isDefinedBy" ;
+	rdfs:comment "The defininition of the subject resource." ;
+	rdfs:range rdfs:Resource ;
+	rdfs:domain rdfs:Resource .
+
+rdfs:Literal a rdfs:Class ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "Literal" ;
+	rdfs:comment "The class of literal values, eg. textual strings and integers." ;
+	rdfs:subClassOf rdfs:Resource .
+
+rdfs:Container a rdfs:Class ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "Container" ;
+	rdfs:subClassOf rdfs:Resource ;
+	rdfs:comment "The class of RDF containers." .
+
+rdfs:ContainerMembershipProperty a rdfs:Class ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "ContainerMembershipProperty" ;
+	rdfs:comment """The class of container membership properties, rdf:_1, rdf:_2, ...,
+                    all of which are sub-properties of 'member'.""" ;
+	rdfs:subClassOf rdf:Property .
+
+rdfs:member a rdf:Property ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "member" ;
+	rdfs:comment "A member of the subject resource." ;
+	rdfs:domain rdfs:Resource ;
+	rdfs:range rdfs:Resource .
+
+rdfs:Datatype a rdfs:Class ;
+	rdfs:isDefinedBy <http://www.w3.org/2000/01/rdf-schema#> ;
+	rdfs:label "Datatype" ;
+	rdfs:comment "The class of RDF datatypes." ;
+	rdfs:subClassOf rdfs:Class .
+
+<http://www.w3.org/2000/01/rdf-schema#> rdfs:seeAlso <http://www.w3.org/2000/01/rdf-schema-more> .
diff --git a/test/test.py b/test/test.py
deleted file mode 100644
index 48eba0d..0000000
--- a/test/test.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import os
-import shutil
-try:
-    from commands import getoutput
-except ImportError:
-    # Python 3
-    from subprocess import getoutput
-
-from rdflib import (
-    BNode,
-    Graph,
-    Literal,
-    RDF,
-    RDFS,
-    URIRef,
-    plugin,
-)
-from rdflib.store import Store
-
-
-# Work in progress
-
-
-def investigate_len_issue():
-    store = plugin.get("SQLAlchemy", Store)(
-        identifier=URIRef("rdflib_test"),
-        configuration=Literal("sqlite:///%(here)s/development.sqlite" % {
-                                                        "here": os.getcwd()}))
-    g0 = Graph("Sleepycat")
-    g0.open("/tmp/foo", create=True)
-    g1 = Graph(store)
-    statementId = BNode()
-    g0.add((statementId, RDF.type, RDF.Statement))
-    g1.add((statementId, RDF.type, RDF.Statement))
-    g0.add((statementId, RDF.subject,
-           URIRef(u"http://rdflib.net/store/ConjunctiveGraph")))
-    g1.add((statementId, RDF.subject,
-           URIRef(u"http://rdflib.net/store/ConjunctiveGraph")))
-    g0.add((statementId, RDF.predicate, RDFS.label))
-    g1.add((statementId, RDF.predicate, RDFS.label))
-    g0.add((statementId, RDF.object, Literal("Conjunctive Graph")))
-    g1.add((statementId, RDF.object, Literal("Conjunctive Graph")))
-    getoutput("cp development.sqlite devcopy.sqlite")
-    g0.remove((statementId, RDF.type, RDF.Statement))
-    g1.remove((statementId, RDF.type, RDF.Statement))
-    g0.close()
-    shutil.rmtree("/tmp/foo")
-    g1.close()
-    os.unlink("%(here)s/development.sqlite" % {"here": os.getcwd()})
-
-
-if __name__ == "__main__":
-    investigate_len_issue()
diff --git a/test/test_aggregate_graphs.py b/test/test_aggregate_graphs.py
index 0c6a3a0..20771e1 100644
--- a/test/test_aggregate_graphs.py
+++ b/test/test_aggregate_graphs.py
@@ -136,7 +136,7 @@ def setUp(self):
                              (testGraph3N3, self.graph3)]:
             graph.parse(StringIO(n3Str), format='n3')
         self.graph4 = Graph(memStore, RDFS.uri)
-        self.graph4.parse(RDFS.uri)
+        self.graph4.parse('test/rdf-schema.n3', format='n3')
         self.G = ConjunctiveGraph(memStore)
 
     def testAggregateSPARQL(self):
diff --git a/test/test_aggregate_graphs_base.py b/test/test_aggregate_graphs_base.py
deleted file mode 100644
index aeb5a42..0000000
--- a/test/test_aggregate_graphs_base.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# import unittest, logging
-# from rdflib import Literal
-# from rdflib import plugin
-# from rdflib import RDF
-# from rdflib import RDFS
-# from rdflib import URIRef
-# from rdflib.store import Store
-# from cStringIO import StringIO
-# from rdflib.graph import Graph
-# from rdflib.graph import ConjunctiveGraph
-# from rdflib.graph import ReadOnlyGraphAggregate
-
-# logging.basicConfig(level=logging.ERROR,format="%(message)s")
-# _logger = logging.getLogger(__name__)
-# _logger.setLevel(logging.DEBUG)
-
-# testGraph1N3="""
-# @prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
-# @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-# @prefix : <http://test/> .
-# :foo a rdfs:Class.
-# :bar :d :c.
-# :a :d :c.
-# """
-
-# testGraph2N3="""
-# @prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
-# @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-# @prefix : <http://test/> .
-# @prefix log: <http://www.w3.org/2000/10/swap/log#>.
-# :foo a rdfs:Resource.
-# :bar rdfs:isDefinedBy [ a log:Formula ].
-# :a :d :e.
-# """
-
-# testGraph3N3="""
-# @prefix rdf:  <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
-# @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
-# @prefix log: <http://www.w3.org/2000/10/swap/log#>.
-# @prefix : <http://test/> .
-# <> a log:N3Document.
-# """
-
-# sparqlQ = \
-# """
-# PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
-# SELECT *
-# FROM NAMED <http://example.com/graph1>
-# FROM NAMED <http://example.com/graph2>
-# FROM NAMED <http://example.com/graph3>
-# FROM <http://www.w3.org/2000/01/rdf-schema#>
-
-# WHERE {?sub ?pred rdfs:Class }"""
-
-# sparqlQ2 =\
-# """
-# PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
-# SELECT ?class
-# WHERE { GRAPH ?graph { ?member a ?class } }"""
-
-# sparqlQ3 =\
-# """
-# PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
-# PREFIX log: <http://www.w3.org/2000/10/swap/log#>
-# SELECT ?n3Doc
-# WHERE {?n3Doc a log:N3Document }"""
-
-# # sqlalchemy_url = Literal('postgresql+psycopg2://gjh@localhost/test')
-# sqlalchemy_url = Literal("mysql://gjh:50uthf0rk@localhost:3306/test")
-# sqlalchemy_url = Literal("postgresql+psycopg2://gjh:50uthf0rk@localhost/test")
-
-# class GraphAggregates1(unittest.TestCase):
-
-#     def setUp(self):
-#         memStore = plugin.get('SQLAlchemyBase', Store)(
-#             identifier="rdflib_test", configuration=sqlalchemy_url)
-#         self.graph1 = Graph(memStore)
-#         self.graph2 = Graph(memStore)
-#         self.graph3 = Graph(memStore)
-
-#         for n3Str, graph in [(testGraph1N3, self.graph1),
-#                              (testGraph2N3, self.graph2),
-#                              (testGraph3N3, self.graph3)]:
-#             graph.parse(StringIO(n3Str), format='n3')
-#             _logger.debug("Graph... %s" % graph.serialize(format="nt"))
-
-#         self.G = ReadOnlyGraphAggregate([self.graph1, self.graph2, self.graph3])
-
-#     def testAggregateRaw(self):
-#         #Test triples
-#         assert len(list(self.G.triples((None, RDF.type, None)))) == 4,
-#              len(list(self.G.triples((None, RDF.type, None))))
-#         assert len(list(self.G.triples((URIRef("http://test/bar"), None, None)))) == 2
-#         assert len(list(self.G.triples((None, URIRef("http://test/d"), None))))   == 3
-
-#         #Test __len__
-#         assert len(self.G) == 8, self.G.serialize(format="nt")
-
-#         #assert context iteration
-#         for g in self.G.contexts():
-#             assert isinstance(g, Graph)
-
-#         #Test __contains__
-#         assert (URIRef("http://test/foo"), RDF.type, RDFS.Resource) in self.G
-
-#         barPredicates = [URIRef("http://test/d"), RDFS.isDefinedBy]
-#         assert len(list(self.G.triples_choices((URIRef("http://test/bar"), barPredicates, None)))) == 2
-
-# class GraphAggregates2(unittest.TestCase):
-#     def setUp(self):
-#         memStore = plugin.get('SQLAlchemyBase', Store)(
-#             identifier="rdflib_test", configuration=sqlalchemy_url)
-#         self.graph1 = Graph(memStore, URIRef("http://example.com/graph1"))
-#         self.graph2 = Graph(memStore, URIRef("http://example.com/graph2"))
-#         self.graph3 = Graph(memStore, URIRef("http://example.com/graph3"))
-
-#         for n3Str,graph in [(testGraph1N3, self.graph1),
-#                             (testGraph2N3, self.graph2),
-#                             (testGraph3N3, self.graph3)]:
-#             graph.parse(StringIO(n3Str), format='n3')
-
-#         self.graph4 = Graph(memStore, RDFS.uri)
-#         self.graph4.parse(RDFS.uri)
-#         self.G = ConjunctiveGraph(memStore)
-
-#     def testAggregateSPARQL(self):
-#         rt =  self.G.query(sparqlQ)
-#         assert len(rt) > 1
-#         rt = self.G.query(sparqlQ2, initBindings={u'?graph' : URIRef(u"http://example.com/graph3")})
-#         try:
-#             import json
-#         except ImportError:
-#             import simplejson as json
-#         res = json.loads(rt.serialize(format='json'))
-#         assert len(res['results']['bindings']) == 20, len(res['results']['bindings'])
-
-# class GraphAggregates3(unittest.TestCase):
-#     def setUp(self):
-#         memStore = plugin.get('SQLAlchemyBase', Store)(
-#             identifier="rdflib_test", configuration=sqlalchemy_url)
-#         self.graph1 = Graph(memStore,URIRef("graph1"))
-#         self.graph2 = Graph(memStore,URIRef("graph2"))
-#         self.graph3 = Graph(memStore,URIRef("graph3"))
-
-#         for n3Str, graph in [(testGraph1N3,self.graph1),
-#                             (testGraph2N3,self.graph2),
-#                             (testGraph3N3,self.graph3)]:
-#             graph.parse(StringIO(n3Str),format='n3')
-#         self.G = ConjunctiveGraph(memStore)
-
-#     def testDefaultGraph(self):
-#         #test that CG includes triples from all 3
-#         assert self.G.query(sparqlQ3), "CG as default graph should *all* triples"
-#         assert not self.graph2.query(sparqlQ3),
-#             "Graph as default graph should *not* include triples from other graphs"
-
-# if __name__ == '__main__':
-#     unittest.main()
diff --git a/test/test_sqlalchemy.py b/test/test_sqlalchemy.py
index 5c673a6..fd89830 100644
--- a/test/test_sqlalchemy.py
+++ b/test/test_sqlalchemy.py
@@ -1,26 +1,27 @@
-import logging
 import unittest
 
+try:
+    from unittest.mock import patch, MagicMock
+except ImportError:
+    from mock import patch, MagicMock
+
+import six
+
 from rdflib import (
     ConjunctiveGraph,
     Literal,
     URIRef,
+    plugin
 )
-from rdflib import plugin
 from rdflib.store import Store
 
 from rdflib_sqlalchemy import registerplugins
+from sqlalchemy.sql.selectable import Select
 
 
-_logger = logging.getLogger(__name__)
-
 michel = URIRef(u"michel")
-tarek = URIRef(u"tarek")
-bob = URIRef(u"bob")
 likes = URIRef(u"likes")
-hates = URIRef(u"hates")
 pizza = URIRef(u"pizza")
-cheese = URIRef(u"cheese")
 
 
 class mock_cursor():
@@ -28,50 +29,99 @@ def execute(x):
         raise Exception("Forced exception")
 
 
+class ConfigTest(unittest.TestCase):
+    '''
+    Test configuration with a dict
+    '''
+
+    def setUp(self):
+        self.store = plugin.get("SQLAlchemy", Store)()
+        self.graph = ConjunctiveGraph(self.store)
+
+    def tearDown(self):
+        self.graph.close()
+
+    def test_success(self):
+        with patch('rdflib_sqlalchemy.store.sqlalchemy') as p:
+            self.graph.open({'url': 'sqlite://', 'random_key': 'something'}, create=True)
+            p.create_engine.assert_called_with('sqlite://', random_key='something')
+
+    def test_no_url(self):
+        with patch('rdflib_sqlalchemy.store.sqlalchemy'):
+            with six.assertRaisesRegex(self, Exception, '.*url.*'):
+                self.graph.open({'random_key': 'something'}, create=True)
+
+
 class SQLATestCase(unittest.TestCase):
     identifier = URIRef("rdflib_test")
     dburi = Literal("sqlite://")
 
     def setUp(self):
         self.store = plugin.get(
-            "SQLAlchemy", Store)(identifier=self.identifier)
+            "SQLAlchemy", Store)(identifier=self.identifier, configuration=self.dburi)
         self.graph = ConjunctiveGraph(self.store, identifier=self.identifier)
         self.graph.open(self.dburi, create=True)
 
     def tearDown(self):
         self.graph.destroy(self.dburi)
-        try:
-            self.graph.close()
-        except:
-            pass
+        self.graph.close()
 
     def test_registerplugins(self):
         # I doubt this is quite right for a fresh pip installation,
         # this test is mainly here to fill a coverage gap.
         registerplugins()
-        self.assert_(plugin.get("SQLAlchemy", Store) is not None)
+        self.assertIsNotNone(plugin.get("SQLAlchemy", Store))
         p = plugin._plugins
-        self.assert_(("SQLAlchemy", Store) in p, p)
+        self.assertIn(("SQLAlchemy", Store), p)
         del p[("SQLAlchemy", Store)]
         plugin._plugins = p
         registerplugins()
-        self.assert_(("SQLAlchemy", Store) in p, p)
+        self.assertIn(("SQLAlchemy", Store), p)
 
     def test_namespaces(self):
-        self.assert_(list(self.graph.namespaces()) != [])
+        self.assertNotEqual(list(self.graph.namespaces()), [])
 
     def test_contexts_without_triple(self):
-        self.assert_(list(self.graph.contexts()) == [])
+        self.assertEqual(list(self.graph.contexts()), [])
+
+    def test_contexts_result(self):
+        ctx_id = URIRef('http://example.org/context')
+        g = self.graph.get_context(ctx_id)
+        g.add((michel, likes, pizza))
+        actual = list(self.store.contexts())
+        self.assertEqual(actual[0], ctx_id)
 
     def test_contexts_with_triple(self):
         statemnt = (michel, likes, pizza)
-        self.assert_(self.graph.contexts(triple=statemnt) != [])
+        self.assertEqual(list(self.graph.contexts(triple=statemnt)), [])
 
     def test__len(self):
-        self.assert_(self.store.__len__() == 0)
+        self.assertEqual(self.store.__len__(), 0)
 
     def test__remove_context(self):
-        self.store._remove_context(self.identifier)
+        ctx_id = URIRef('http://example.org/context')
+        g = self.graph.get_context(ctx_id)
+        g.add((michel, likes, pizza))
+        self.store._remove_context(g)
+        self.assertEqual(list(self.store.contexts()), [])
+
+    def test_triples_choices(self):
+        # Create a mock for the sqlalchemy engine so we can capture the arguments
+        p = MagicMock(name='engine')
+        self.store.engine = p
+
+        # Set this so we're not including selects for both asserted and literal tables for
+        # a choice
+        self.store.STRONGLY_TYPED_TERMS = True
+        # Set the grouping of terms
+        self.store.max_terms_per_where = 2
+        # force execution of the generator
+        for x in self.store.triples_choices((None, likes, [michel, pizza, likes])):
+            pass
+        args = p.connect().__enter__().execute.call_args[0]
+        children = args[0].get_children(column_collections=False)
+        # Expect two selects: one for the first two choices plus one for the last one
+        self.assertEqual(sum(1 for c in children if isinstance(c, Select)), 2)
 
 
 if __name__ == "__main__":
diff --git a/test/test_sqlalchemy_mysql.py b/test/test_sqlalchemy_mysql.py
index 18fe193..d47a77c 100644
--- a/test/test_sqlalchemy_mysql.py
+++ b/test/test_sqlalchemy_mysql.py
@@ -5,11 +5,10 @@
 from nose import SkipTest
 from rdflib import Literal
 from rdflib.graph import ConjunctiveGraph as Graph
-from rdflib.py3compat import PY3
+from six import PY3
 from rdflib.store import NO_STORE, VALID_STORE
 from rdflib.term import URIRef
 
-
 from . import context_case
 from . import graph_case
 
@@ -48,8 +47,7 @@ class SQLAMySQLGraphTestCase(graph_case.GraphTestCase):
     create = True
 
     def setUp(self):
-        super(SQLAMySQLGraphTestCase, self).setUp(
-            uri=self.uri, storename=self.storename)
+        super(SQLAMySQLGraphTestCase, self).setUp(uri=self.uri, storename=self.storename)
 
     def tearDown(self):
         super(SQLAMySQLGraphTestCase, self).tearDown(uri=self.uri)
@@ -84,13 +82,9 @@ def test_issue_4(self):
         if rt == NO_STORE:
             g.open(self.uri, create=True)
         else:
-            assert rt == VALID_STORE, "The underlying store is corrupt"
+            assert rt == VALID_STORE, "The underlying store is not valid: State: %s" % rt
         g.destroy(self.uri)
 
 
-SQLAMySQLGraphTestCase.storetest = True
-SQLAMySQLContextTestCase.storetest = True
-SQLAMySQLIssueTestCase.storetest = True
-
 if __name__ == "__main__":
     unittest.main()