Mike Bayer avatar Mike Bayer committed 83d8614

initial rev

Comments (0)

Files changed (15)

+syntax:regexp
+^build/
+^dist/
+^docs/build/output
+.pyc$
+.orig$
+.egg-info
+.coverage
+alembic.ini
+local_test
+This is the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+Copyright (C) 2005-2012 by Jonathan Ellis and Michael Bayer.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+recursive-include docs *.html *.css *.js *.txt
+recursive-include tests *.py *.dat
+
+include README* LICENSE distribute_setup.py CHANGES* test.cfg
+
+prune docs/build/output
+
+
+=======
+SQLSoup
+=======
+
+SQLSoup provides a convenient way to map Python objects
+to relational database tables, with no declarative code
+of any kind.   It's built on top of the 
+`SQLAlchemy <http://www.sqlalchemy.org>`_ ORM and provides a
+super-minimalistic interface to an existing database.
+
+Usage is as simple as::
+
+    import sqlsoup
+    db = sqlsoup.SQLSoup("postgresql://scott:tiger@localhost/test")
+
+    for user in db.users:
+        print "user:", user.name
+
+    db.users.filter_by(name="ed").update({"name":"jack"})
+    db.commit()
+
+Included for many years as an extension to SQLAlchemy itself, SQLSoup 
+has been broken out into it's own project as of 2012.   The community is encouraged
+to collaborate on Bitbucket with patches and features.
+
+Documentation and status of SQLSoup is at http://readthedocs.org/docs/sqlsoup/.

docs/build/Makefile

+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = output
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SQLSoup.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SQLSoup.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/SQLSoup"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SQLSoup"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."

docs/build/api.rst

+===========
+API Details
+===========
+
+This section includes generated documentation for the :class:`.SQLSoup` object.
+
+.. automodule:: sqlsoup
+    :members:

docs/build/conf.py

+# -*- coding: utf-8 -*-
+#
+# SQLSoup documentation build configuration file, created by
+# sphinx-quickstart on Sun Jan  8 12:07:16 2012.
+#
+# 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
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# 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('.'))
+sys.path.insert(0, os.path.abspath('../../'))
+
+import sqlsoup
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# 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.intersphinx', 'sphinx.ext.viewcode']
+
+# 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'SQLSoup'
+copyright = u'2005-2012, Jonathan Ellis, Mike Bayer'
+
+# 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.
+version = '0.9'
+# The full version, including alpha/beta/rc tags.
+release = '0.9'
+
+# 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 = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# 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'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'nature'
+
+# 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 = {}
+
+# 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 = {}
+
+# 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 = 'SQLSoupdoc'
+
+
+# -- 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', 'SQLSoup.tex', u'SQLSoup Documentation',
+   u'Jonathan Ellis, Mike Bayer', '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', 'sqlsoup', u'SQLSoup Documentation',
+     [u'Jonathan Ellis, Mike Bayer'], 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', 'SQLSoup', u'SQLSoup Documentation',
+   u'Jonathan Ellis, Mike Bayer', 'SQLSoup', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# 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'
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+#intersphinx_mapping = {'http://docs.python.org/': None}
+
+intersphinx_mapping = {'sqla':('http://www.sqlalchemy.org/docs/', None)}

docs/build/front.rst

+============
+Front Matter
+============
+
+Information about the SQLSoup project.
+
+Project Homepage
+================
+
+SQLSoup is hosted on `Bitbucket <http://bitbucket.org>`_ - the lead project
+page is at https://bitbucket.org/zzzeek/sqlsoup. Source code is tracked here
+using `Mercurial <http://mercurial.selenic.com/>`_.
+
+Releases and project status are available on Pypi at
+http://pypi.python.org/pypi/sqlsoup.
+
+The most recent published version of this documentation should be at
+http://readthedocs.org/docs/sqlsoup/.
+
+Project Status
+==============
+
+SQLSoup has been included with the SQLAlchemy distribution for many years.
+As of 2012 it has been broken out into it's own project, where it can
+be tracked and released separately.   The community is also invited to 
+collaborate on Bitbucket, create forks, and improve the product.
+
+.. _installation:
+
+Installation
+============
+
+Install released versions of Alembic from the Python package 
+index with `pip <http://pypi.python.org/pypi/pip>`_ or a similar tool::
+
+    pip install sqlsoup
+
+Installation via source distribution is via the ``setup.py`` script::
+
+    python setup.py install
+
+Dependencies
+------------
+
+SQLSoup's install process will ensure that `SQLAlchemy <http://www.sqlalchemy.org>`_ 
+is installed, in addition to other dependencies.  The 0.7 series of 
+SQLAlchemy or greater is recommended.
+
+
+Community
+=========
+
+SQLSoup was originally written by Jonathan Ellis.   It is under maintenance
+by `Mike Bayer <http://techspot.zzzeek.org>`_, and is 
+loosely associated with the `SQLAlchemy`_ project. 
+
+.. _bugs:
+
+Bugs
+====
+
+Bugs and feature enhancements to SQLSoup should be reported on the `Bitbucket
+issue tracker <https://bitbucket.org/zzzeek/sqlsoup/issues?status=new&status=open>`_.  

docs/build/index.rst

+Welcome to SQLSoup's documentation!
+===================================
+
+`SQLSoup <http://bitbucket.org/zzzeek/sqlsoup>`_ is a one step database access tool
+built on top of the `SQLAlchemy <http://www.sqlalchemy.org>`_ Object Relational Mapper.
+
+.. toctree::
+   :maxdepth: 2
+
+   front
+   tutorial
+   api
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+

docs/build/make.bat

+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
+set I18NSPHINXOPTS=%SPHINXOPTS% source
+if NOT "%PAPER%" == "" (
+	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+	set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+	:help
+	echo.Please use `make ^<target^>` where ^<target^> is one of
+	echo.  html       to make standalone HTML files
+	echo.  dirhtml    to make HTML files named index.html in directories
+	echo.  singlehtml to make a single large HTML file
+	echo.  pickle     to make pickle files
+	echo.  json       to make JSON files
+	echo.  htmlhelp   to make HTML files and a HTML help project
+	echo.  qthelp     to make HTML files and a qthelp project
+	echo.  devhelp    to make HTML files and a Devhelp project
+	echo.  epub       to make an epub
+	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+	echo.  text       to make text files
+	echo.  man        to make manual pages
+	echo.  texinfo    to make Texinfo files
+	echo.  gettext    to make PO message catalogs
+	echo.  changes    to make an overview over all changed/added/deprecated items
+	echo.  linkcheck  to check all external links for integrity
+	echo.  doctest    to run all doctests embedded in the documentation if enabled
+	goto end
+)
+
+if "%1" == "clean" (
+	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+	del /q /s %BUILDDIR%\*
+	goto end
+)
+
+if "%1" == "html" (
+	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+	goto end
+)
+
+if "%1" == "dirhtml" (
+	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+	goto end
+)
+
+if "%1" == "singlehtml" (
+	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+	goto end
+)
+
+if "%1" == "pickle" (
+	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the pickle files.
+	goto end
+)
+
+if "%1" == "json" (
+	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can process the JSON files.
+	goto end
+)
+
+if "%1" == "htmlhelp" (
+	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+	goto end
+)
+
+if "%1" == "qthelp" (
+	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SQLSoup.qhcp
+	echo.To view the help file:
+	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SQLSoup.ghc
+	goto end
+)
+
+if "%1" == "devhelp" (
+	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished.
+	goto end
+)
+
+if "%1" == "epub" (
+	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The epub file is in %BUILDDIR%/epub.
+	goto end
+)
+
+if "%1" == "latex" (
+	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+	goto end
+)
+
+if "%1" == "text" (
+	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The text files are in %BUILDDIR%/text.
+	goto end
+)
+
+if "%1" == "man" (
+	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The manual pages are in %BUILDDIR%/man.
+	goto end
+)
+
+if "%1" == "texinfo" (
+	%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+	goto end
+)
+
+if "%1" == "gettext" (
+	%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+	goto end
+)
+
+if "%1" == "changes" (
+	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.The overview file is in %BUILDDIR%/changes.
+	goto end
+)
+
+if "%1" == "linkcheck" (
+	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+	goto end
+)
+
+if "%1" == "doctest" (
+	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+	if errorlevel 1 exit /b 1
+	echo.
+	echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+	goto end
+)
+
+:end

docs/build/tutorial.rst

+=========
+Tutorial
+=========
+
+SQLSoup provides a convenient way to access existing database
+tables without having to declare table or mapper classes ahead
+of time. It is built on top of the SQLAlchemy ORM and provides a
+super-minimalistic interface to an existing database.
+
+SQLSoup effectively provides a coarse grained, alternative
+interface to working with the SQLAlchemy ORM, providing a "self
+configuring" interface for extremely rudimental operations. It's
+somewhat akin to a "super novice mode" version of the ORM. While
+SQLSoup can be very handy, users are strongly encouraged to use
+the full ORM for non-trivial applications.
+
+Getting Ready to Connect
+=========================
+
+Suppose we have a database with users, books, and loans tables
+(corresponding to the PyWebOff dataset, if you're curious).
+
+Creating a SQLSoup gateway is just like creating an SQLAlchemy
+engine::
+
+    >>> import sqlsoup
+    >>> db = sqlsoup.SQLSoup('sqlite:///:memory:')
+
+or, you can re-use an existing engine::
+
+    >>> db = sqlsoup.SQLSoup(engine)
+
+You can optionally specify a schema within the database for your
+SQLSoup::
+
+    >>> db.schema = myschemaname
+
+Note that the :class:`.SQLSoup` object doesn't actually connect
+to the database until it's first asked to do something.  If the connection
+string is incorrect, the error will be raised when SQLSoup first tries
+to connect.
+
+Loading objects
+===============
+
+Loading objects is as easy as this::
+
+    >>> users = db.users.all()
+    >>> users.sort()
+    >>> users
+    [
+        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+                password=u'student',classname=None,admin=0), 
+        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+                password=u'basepair',classname=None,admin=1)
+    ]
+
+Of course, letting the database do the sort is better::
+
+    >>> db.users.order_by(db.users.name).all()
+    [
+        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+            password=u'basepair',classname=None,admin=1), 
+        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0)
+    ]
+
+Field access is intuitive::
+
+    >>> users[0].email
+    u'student@example.edu'
+
+Of course, you don't want to load all users very often. Let's
+add a WHERE clause. Let's also switch the order_by to DESC while
+we're at it::
+
+    >>> from sqlalchemy import or_, and_, desc
+    >>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu')
+    >>> db.users.filter(where).order_by(desc(db.users.name)).all()
+    [
+        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0), 
+        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+            password=u'basepair',classname=None,admin=1)
+    ]
+
+You can also use .first() (to retrieve only the first object
+from a query) or .one() (like .first when you expect exactly one
+user -- it will raise an exception if more were returned)::
+
+    >>> db.users.filter(db.users.name=='Bhargan Basepair').one()
+    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+            password=u'basepair',classname=None,admin=1)
+
+Since name is the primary key, this is equivalent to
+
+    >>> db.users.get('Bhargan Basepair')
+    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+        password=u'basepair',classname=None,admin=1)
+
+This is also equivalent to
+
+    >>> db.users.filter_by(name='Bhargan Basepair').one()
+    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+        password=u'basepair',classname=None,admin=1)
+
+filter_by is like filter, but takes kwargs instead of full
+clause expressions. This makes it more concise for simple
+queries like this, but you can't do complex queries like the
+or\_ above or non-equality based comparisons this way.
+
+Full query documentation
+------------------------
+
+Get, filter, filter_by, order_by, limit, and the rest of the
+query methods are explained in detail in
+:ref:`ormtutorial_querying`.
+
+Modifying objects
+=================
+
+Modifying objects is intuitive::
+
+    >>> user = _
+    >>> user.email = 'basepair+nospam@example.edu'
+    >>> db.commit()
+
+(SQLSoup leverages the sophisticated SQLAlchemy unit-of-work
+code, so multiple updates to a single object will be turned into
+a single ``UPDATE`` statement when you commit.)
+
+To finish covering the basics, let's insert a new loan, then
+delete it::
+
+    >>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id
+    >>> db.loans.insert(book_id=book_id, user_name=user.name)
+    MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
+
+    >>> loan = db.loans.filter_by(book_id=2, user_name='Bhargan Basepair').one()
+    >>> db.delete(loan)
+    >>> db.commit()
+
+You can also delete rows that have not been loaded as objects.
+Let's do our insert/delete cycle once more, this time using the
+loans table's delete method. (For SQLAlchemy experts: note that
+no flush() call is required since this delete acts at the SQL
+level, not at the Mapper level.) The same where-clause
+construction rules apply here as to the select methods::
+
+    >>> db.loans.insert(book_id=book_id, user_name=user.name)
+    MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
+    >>> db.loans.delete(db.loans.book_id==2)
+
+You can similarly update multiple rows at once. This will change the
+book_id to 1 in all loans whose book_id is 2::
+
+    >>> db.loans.filter_by(db.loans.book_id==2).update({'book_id':1})
+    >>> db.loans.filter_by(book_id=1).all()
+    [MappedLoans(book_id=1,user_name=u'Joe Student',
+        loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+
+
+Joins
+=====
+
+Occasionally, you will want to pull out a lot of data from related
+tables all at once.  In this situation, it is far more efficient to
+have the database perform the necessary join.  (Here we do not have *a
+lot of data* but hopefully the concept is still clear.)  SQLAlchemy is
+smart enough to recognize that loans has a foreign key to users, and
+uses that as the join condition automatically::
+
+    >>> join1 = db.join(db.users, db.loans, isouter=True)
+    >>> join1.filter_by(name='Joe Student').all()
+    [
+        MappedJoin(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0,book_id=1,
+            user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))
+    ]
+
+If you're unfortunate enough to be using MySQL with the default MyISAM
+storage engine, you'll have to specify the join condition manually,
+since MyISAM does not store foreign keys.  Here's the same join again,
+with the join condition explicitly specified::
+
+    >>> db.join(db.users, db.loans, db.users.name==db.loans.user_name, isouter=True)
+    <class 'sqlsoup.MappedJoin'>
+
+You can compose arbitrarily complex joins by combining Join objects
+with tables or other joins.  Here we combine our first join with the
+books table::
+
+    >>> join2 = db.join(join1, db.books)
+    >>> join2.all()
+    [
+        MappedJoin(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0,book_id=1,
+            user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),
+            id=1,title=u'Mustards I Have Known',published_year=u'1989',
+            authors=u'Jones')
+    ]
+
+If you join tables that have an identical column name, wrap your join
+with `with_labels`, to disambiguate columns with their table name
+(.c is short for .columns)::
+
+    >>> db.with_labels(join1).c.keys()
+    [u'users_name', u'users_email', u'users_password', 
+        u'users_classname', u'users_admin', u'loans_book_id', 
+        u'loans_user_name', u'loans_loan_date']
+
+You can also join directly to a labeled object::
+
+    >>> labeled_loans = db.with_labels(db.loans)
+    >>> db.join(db.users, labeled_loans, isouter=True).c.keys()
+    [u'name', u'email', u'password', u'classname', 
+        u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
+
+
+Relationships
+=============
+
+You can define relationships between classes using the :meth:`.TableClassType.relate`
+method from any mapped table:
+
+    >>> db.users.relate('loans', db.loans)
+
+These can then be used like a normal SA property:
+
+    >>> db.users.get('Joe Student').loans
+    [MappedLoans(book_id=1,user_name=u'Joe Student',
+                    loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+
+    >>> db.users.filter(~db.users.loans.any()).all()
+    [MappedUsers(name=u'Bhargan Basepair',
+            email='basepair+nospam@example.edu',
+            password=u'basepair',classname=None,admin=1)]
+
+relate can take any options that the relationship function
+accepts in normal mapper definition:
+
+    >>> del db._cache['users']
+    >>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
+
+Advanced Use
+============
+
+Sessions, Transations and Application Integration
+-------------------------------------------------
+
+.. note::
+
+   Please read and understand this section thoroughly
+   before using SQLSoup in any web application.
+
+SQLSoup uses a :class:`sqlalchemy.orm.scoping.ScopedSession` to provide thread-local sessions.
+You can get a reference to the current one like this::
+
+    >>> session = db.session
+
+The default session is available at the module level in SQLSoup,
+via::
+
+    >>> from sqlsoup import Session
+
+The configuration of this session is ``autoflush=True``,
+``autocommit=False``. This means when you work with the SQLSoup
+object, you need to call ``db.commit()`` in order to have
+changes persisted. You may also call ``db.rollback()`` to roll
+things back.
+
+Since the SQLSoup object's Session automatically enters into a
+transaction as soon as it's used, it is *essential* that you
+call ``commit()`` or ``rollback()`` on it when the work within a
+thread completes. This means all the guidelines for web
+application integration at :ref:`session_lifespan` must be
+followed.
+
+The SQLSoup object can have any session or scoped session
+configured onto it. This is of key importance when integrating
+with existing code or frameworks such as Pylons. If your
+application already has a ``Session`` configured, pass it to
+your SQLSoup object::
+
+    >>> from myapplication import Session
+    >>> db = SQLSoup(session=Session)
+
+If the ``Session`` is configured with ``autocommit=True``, use
+``flush()`` instead of ``commit()`` to persist changes - in this
+case, the ``Session`` closes out its transaction immediately and
+no external management is needed. ``rollback()`` is also not
+available. Configuring a new SQLSoup object in "autocommit" mode
+looks like::
+
+    >>> from sqlalchemy.orm import scoped_session, sessionmaker
+    >>> db = SQLSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
+
+
+Mapping arbitrary Selectables
+-----------------------------
+
+SQLSoup can map any SQLAlchemy :class:`.Selectable` with the map
+method. Let's map an :func:`.expression.select` object that uses an aggregate
+function; we'll use the SQLAlchemy :class:`.Table` that SQLSoup
+introspected as the basis. (Since we're not mapping to a simple
+table or join, we need to tell SQLAlchemy how to find the
+*primary key* which just needs to be unique within the select,
+and not necessarily correspond to a *real* PK in the database.)::
+
+    >>> from sqlalchemy import select, func
+    >>> b = db.books._table
+    >>> s = select([b.c.published_year, func.count('*').label('n')], from_obj=[b], group_by=[b.c.published_year])
+    >>> s = s.alias('years_with_count')
+    >>> years_with_count = db.map(s, primary_key=[s.c.published_year])
+    >>> years_with_count.filter_by(published_year='1989').all()
+    [MappedBooks(published_year=u'1989',n=1)]
+
+Obviously if we just wanted to get a list of counts associated with
+book years once, raw SQL is going to be less work. The advantage of
+mapping a Select is reusability, both standalone and in Joins. (And if
+you go to full SQLAlchemy, you can perform mappings like this directly
+to your object models.)
+
+An easy way to save mapped selectables like this is to just hang them on
+your db object::
+
+    >>> db.years_with_count = years_with_count
+
+Python is flexible like that!
+
+Raw SQL
+-------
+
+SQLSoup works fine with SQLAlchemy's text construct, described
+in :ref:`sqlexpression_text`. You can also execute textual SQL
+directly using the :meth:`.SQLSoup.execute` method, which corresponds to the
+:meth:`sqlalchemy.orm.session.Session.execute` method on the underlying :class:`sqlalchemy.orm.session.Session`. Expressions here
+are expressed like :func:`sqlalchemy.sql.expression.text` constructs, using named parameters
+with colons::
+
+    >>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%')
+    >>> for name, email in rp.fetchall(): print name, email
+    Bhargan Basepair basepair+nospam@example.edu
+
+Or you can get at the current transaction's connection using
+:meth:`.SQLSoup.connection`. This is the raw connection object which can
+accept any sort of SQL expression or raw SQL string passed to
+the database::
+
+    >>> conn = db.connection()
+    >>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%')
+
+Dynamic table names
+-------------------
+
+You can load a table whose name is specified at runtime with the
+entity() method:
+
+    >>> tablename = 'loans'
+    >>> db.entity(tablename) == db.loans
+    True
+
+entity() also takes an optional schema argument. If none is
+specified, the default schema is used.
+from setuptools import setup, find_packages
+import sys
+import os
+import re
+
+extra = {}
+if sys.version_info >= (3, 0):
+    extra.update(
+        use_2to3=True,
+    )
+
+v = open(os.path.join(os.path.dirname(__file__), 'sqlsoup', '__init__.py'))
+VERSION = re.compile(r".*__version__ = '(.*?)'", re.S).match(v.read()).group(1)
+v.close()
+
+readme = os.path.join(os.path.dirname(__file__), 'README.rst')
+
+
+setup(name='sqlsoup',
+      version=VERSION,
+      description="A one step database access tool, built on the SQLAlchemy ORM.",
+      long_description=open(readme).read(),
+      classifiers=[
+      'Development Status :: 4 - Beta',
+      'Environment :: Console',
+      'Intended Audience :: Developers',
+      'Programming Language :: Python',
+      'Programming Language :: Python :: 3',
+      'Programming Language :: Python :: Implementation :: CPython',
+      'Programming Language :: Python :: Implementation :: PyPy',
+      'Topic :: Database :: Front-Ends',
+      ],
+      keywords='SQLAlchemy ORM',
+      author='Mike Bayer',
+      author_email='mike@zzzcomputing.com',
+      url='http://bitbucket.org/zzzeek/sqlsoup',
+      license='MIT',
+      packages=find_packages('.', exclude=['test*']),
+      include_package_data=True,
+      tests_require = ['nose >= 0.11'],
+      test_suite = "nose.collector",
+      zip_safe=False,
+      install_requires=[
+          'SQLAlchemy>=0.7.0',
+      ],
+      **extra
+)
+"""
+
+"""
+
+from sqlalchemy import Table, MetaData, join
+from sqlalchemy import schema, sql, util
+from sqlalchemy.engine.base import Engine
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
+                            class_mapper, relationship, session,\
+                            object_session, attributes
+from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE
+from sqlalchemy.sql import expression
+
+__version__ = "0.9"
+__all__ = ['SQLSoupError', 'SQLSoup', 'SelectableClassType', 'TableClassType']
+
+Session = scoped_session(sessionmaker())
+"""SQLSoup's default session registry.
+
+This is an instance of :class:`sqlalchemy.orm.scoping.ScopedSession`,
+and provides a new :class:`sqlalchemy.orm.session.Session`
+object for each application thread which refers to it.
+
+"""
+
+class AutoAdd(MapperExtension):
+    def __init__(self, scoped_session):
+        self.scoped_session = scoped_session
+
+    def instrument_class(self, mapper, class_):
+        class_.__init__ = self._default__init__(mapper)
+
+    def _default__init__(ext, mapper):
+        def __init__(self, **kwargs):
+            for key, value in kwargs.iteritems():
+                setattr(self, key, value)
+        return __init__
+
+    def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
+        session = self.scoped_session()
+        state = attributes.instance_state(instance)
+        session._save_impl(state)
+        return EXT_CONTINUE
+
+    def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
+        sess = object_session(instance)
+        if sess:
+            sess.expunge(instance)
+        return EXT_CONTINUE
+
+class SQLSoupError(Exception):
+    pass
+
+class ArgumentError(SQLSoupError):
+    pass
+
+# metaclass is necessary to expose class methods with getattr, e.g.
+# we want to pass db.users.select through to users._mapper.select
+class SelectableClassType(type):
+    """Represent a SQLSoup mapping to a :class:`sqlalchemy.sql.expression.Selectable`
+    construct, such as a table or SELECT statement.
+    
+    """
+
+    def insert(cls, **kwargs):
+        raise SQLSoupError(
+            'SQLSoup can only modify mapped Tables (found: %s)' \
+              % cls._table.__class__.__name__
+        )
+
+    def __clause_element__(cls):
+        return cls._table
+
+    def __getattr__(cls, attr):
+        if attr == '_query':
+            # called during mapper init
+            raise AttributeError()
+        return getattr(cls._query, attr)
+
+class TableClassType(SelectableClassType):
+    """Represent a SQLSoup mapping to a :class:`sqlalchemy.schema.Table`
+    construct.
+    
+    This object is produced automatically when a table-name
+    attribute is accessed from a :class:`.SQLSoup` instance.
+    
+    """
+    def insert(cls, **kwargs):
+        o = cls()
+        o.__dict__.update(kwargs)
+        return o
+
+    def relate(cls, propname, *args, **kwargs):
+        """Produce a relationship between this mapped table and another
+        one. 
+        
+        This makes usage of SQLAlchemy's :func:`sqlalchemy.orm.relationship`
+        construct.
+        
+        """
+        class_mapper(cls)._configure_property(propname, relationship(*args, **kwargs))
+
+def _is_outer_join(selectable):
+    if not isinstance(selectable, sql.Join):
+        return False
+    if selectable.isouter:
+        return True
+    return _is_outer_join(selectable.left) or _is_outer_join(selectable.right)
+
+def _selectable_name(selectable):
+    if isinstance(selectable, sql.Alias):
+        return _selectable_name(selectable.element)
+    elif isinstance(selectable, sql.Select):
+        return ''.join(_selectable_name(s) for s in selectable.froms)
+    elif isinstance(selectable, schema.Table):
+        return selectable.name.capitalize()
+    else:
+        x = selectable.__class__.__name__
+        if x[0] == '_':
+            x = x[1:]
+        return x
+
+def _class_for_table(session, engine, selectable, base_cls, mapper_kwargs):
+    selectable = expression._clause_element_as_expr(selectable)
+    mapname = 'Mapped' + _selectable_name(selectable)
+    # Py2K
+    if isinstance(mapname, unicode): 
+        engine_encoding = engine.dialect.encoding 
+        mapname = mapname.encode(engine_encoding)
+    # end Py2K
+
+    if isinstance(selectable, Table):
+        klass = TableClassType(mapname, (base_cls,), {})
+    else:
+        klass = SelectableClassType(mapname, (base_cls,), {})
+
+    def _compare(self, o):
+        L = list(self.__class__.c.keys())
+        L.sort()
+        t1 = [getattr(self, k) for k in L]
+        try:
+            t2 = [getattr(o, k) for k in L]
+        except AttributeError:
+            raise TypeError('unable to compare with %s' % o.__class__)
+        return t1, t2
+
+    # python2/python3 compatible system of 
+    # __cmp__ - __lt__ + __eq__
+
+    def __lt__(self, o):
+        t1, t2 = _compare(self, o)
+        return t1 < t2
+
+    def __eq__(self, o):
+        t1, t2 = _compare(self, o)
+        return t1 == t2
+
+    def __repr__(self):
+        L = ["%s=%r" % (key, getattr(self, key, ''))
+             for key in self.__class__.c.keys()]
+        return '%s(%s)' % (self.__class__.__name__, ','.join(L))
+
+    for m in ['__eq__', '__repr__', '__lt__']:
+        setattr(klass, m, eval(m))
+    klass._table = selectable
+    klass.c = expression.ColumnCollection()
+    mappr = mapper(klass,
+                   selectable,
+                   extension=AutoAdd(session),
+                   **mapper_kwargs)
+
+    for k in mappr.iterate_properties:
+        klass.c[k.key] = k.columns[0]
+
+    klass._query = session.query_property()
+    return klass
+
+class SQLSoup(object):
+    """Represent an ORM-wrapped database resource."""
+
+    def __init__(self, engine_or_metadata, base=object, session=None):
+        """Initialize a new :class:`.SQLSoup`.
+
+        :param engine_or_metadata: a string database URL, :class:`.Engine` 
+          or :class:`.MetaData` object to associate with. If the
+          argument is a :class:`.MetaData`, it should be *bound*
+          to an :class:`.Engine`.
+        :param base: a class which will serve as the default class for 
+          returned mapped classes.  Defaults to ``object``.
+        :param session: a :class:`.ScopedSession` or :class:`.Session` with
+          which to associate ORM operations for this :class:`.SQLSoup` instance.
+          If ``None``, a :class:`.ScopedSession` that's local to this 
+          module is used.
+
+        """
+
+        self.session = session or Session
+        self.base=base
+
+        if isinstance(engine_or_metadata, MetaData):
+            self._metadata = engine_or_metadata
+        elif isinstance(engine_or_metadata, (basestring, Engine)):
+            self._metadata = MetaData(engine_or_metadata)
+        else:
+            raise ArgumentError("invalid engine or metadata argument %r" % 
+                                engine_or_metadata)
+
+        self._cache = {}
+        self.schema = None
+
+    @property
+    def bind(self):
+        """The :class:`sqlalchemy.engine.base.Engine` associated with this :class:`.SQLSoup`."""
+        return self._metadata.bind
+
+    engine = bind
+
+    def delete(self, instance):
+        """Mark an instance as deleted."""
+
+        self.session.delete(instance)
+
+    def execute(self, stmt, **params):
+        """Execute a SQL statement.
+
+        The statement may be a string SQL string,
+        an :func:`sqlalchemy.sql.expression.select` construct, or a 
+        :func:`sqlalchemy.sql.expression.text` 
+        construct.
+
+        """
+        return self.session.execute(sql.text(stmt, bind=self.bind), **params)
+
+    @property
+    def _underlying_session(self):
+        if isinstance(self.session, session.Session):
+            return self.session
+        else:
+            return self.session()
+
+    def connection(self):
+        """Return the current :class:`sqlalchemy.engine.base.Connection` in use by the current transaction."""
+
+        return self._underlying_session._connection_for_bind(self.bind)
+
+    def flush(self):
+        """Flush pending changes to the database.
+
+        See :meth:`sqlalchemy.orm.session.Session.flush`.
+
+        """
+        self.session.flush()
+
+    def rollback(self):
+        """Rollback the current transction.
+
+        See :meth:`sqlalchemy.orm.session.Session.rollback`.
+
+        """
+        self.session.rollback()
+
+    def commit(self):
+        """Commit the current transaction.
+
+        See :meth:`sqlalchemy.orm.session.Session.commit`.
+
+        """
+        self.session.commit()
+
+    def expunge(self, instance):
+        """Remove an instance from the :class:`.Session`.
+
+        See :meth:`sqlalchemy.orm.session.Session.expunge`.
+
+        """
+        self.session.expunge(instance)
+
+    def expunge_all(self):
+        """Clear all objects from the current :class:`.Session`.
+
+        See :meth:`.Session.expunge_all`.
+
+        """
+        self.session.expunge_all()
+
+    def map_to(self, attrname, tablename=None, selectable=None, 
+                    schema=None, base=None, mapper_args=util.immutabledict()):
+        """Configure a mapping to the given attrname.
+
+        This is the "master" method that can be used to create any 
+        configuration.
+
+        :param attrname: String attribute name which will be
+          established as an attribute on this :class:.`.SQLSoup`
+          instance.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SQLSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+        :param tablename: String name of a :class:`.Table` to be
+          reflected. If a :class:`.Table` is already available,
+          use the ``selectable`` argument. This argument is
+          mutually exclusive versus the ``selectable`` argument.
+        :param selectable: a :class:`.Table`, :class:`.Join`, or
+          :class:`.Select` object which will be mapped. This
+          argument is mutually exclusive versus the ``tablename``
+          argument.
+        :param schema: String schema name to use if the
+          ``tablename`` argument is present.
+
+
+        """
+        if attrname in self._cache:
+            raise SQLSoupError(
+                "Attribute '%s' is already mapped to '%s'" % (
+                attrname,
+                class_mapper(self._cache[attrname]).mapped_table
+            ))
+
+        if tablename is not None:
+            if not isinstance(tablename, basestring):
+                raise ArgumentError("'tablename' argument must be a string."
+                                    )
+            if selectable is not None:
+                raise ArgumentError("'tablename' and 'selectable' "
+                                    "arguments are mutually exclusive")
+
+            selectable = Table(tablename, 
+                                        self._metadata, 
+                                        autoload=True, 
+                                        autoload_with=self.bind, 
+                                        schema=schema or self.schema)
+        elif schema:
+            raise ArgumentError("'tablename' argument is required when "
+                                "using 'schema'.")
+        elif selectable is not None:
+            if not isinstance(selectable, expression.FromClause):
+                raise ArgumentError("'selectable' argument must be a "
+                                    "table, select, join, or other "
+                                    "selectable construct.")
+        else:
+            raise ArgumentError("'tablename' or 'selectable' argument is "
+                                    "required.")
+
+        if not selectable.primary_key.columns:
+            if tablename:
+                raise SQLSoupError(
+                            "table '%s' does not have a primary "
+                            "key defined" % tablename)
+            else:
+                raise SQLSoupError(
+                            "selectable '%s' does not have a primary "
+                            "key defined" % selectable)
+
+        mapped_cls = _class_for_table(
+            self.session,
+            self.engine,
+            selectable,
+            base or self.base,
+            mapper_args
+        )
+        self._cache[attrname] = mapped_cls
+        return mapped_cls
+
+
+    def map(self, selectable, base=None, **mapper_args):
+        """Map a selectable directly.
+
+        The class and its mapping are not cached and will
+        be discarded once dereferenced (as of 0.6.6).
+
+        :param selectable: an :func:`.expression.select` construct.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SQLSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+
+        """
+
+        return _class_for_table(
+            self.session,
+            self.engine,
+            selectable,
+            base or self.base,
+            mapper_args
+        )
+
+    def with_labels(self, selectable, base=None, **mapper_args):
+        """Map a selectable directly, wrapping the 
+        selectable in a subquery with labels.
+
+        The class and its mapping are not cached and will
+        be discarded once dereferenced (as of 0.6.6).
+
+        :param selectable: an :func:`.expression.select` construct.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SQLSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+
+        """
+
+        # TODO give meaningful aliases
+        return self.map(
+                    expression._clause_element_as_expr(selectable).
+                            select(use_labels=True).
+                            alias('foo'), base=base, **mapper_args)
+
+    def join(self, left, right, onclause=None, isouter=False, 
+                base=None, **mapper_args):
+        """Create an :func:`.expression.join` and map to it.
+
+        The class and its mapping are not cached and will
+        be discarded once dereferenced (as of 0.6.6).
+
+        :param left: a mapped class or table object.
+        :param right: a mapped class or table object.
+        :param onclause: optional "ON" clause construct..
+        :param isouter: if True, the join will be an OUTER join.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SQLSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+
+        """
+
+        j = join(left, right, onclause=onclause, isouter=isouter)
+        return self.map(j, base=base, **mapper_args)
+
+    def entity(self, attr, schema=None):
+        """Return the named entity from this :class:`.SQLSoup`, or 
+        create if not present.
+
+        For more generalized mapping, see :meth:`.map_to`.
+
+        """
+        try:
+            return self._cache[attr]
+        except KeyError, ke:
+            return self.map_to(attr, tablename=attr, schema=schema)
+
+    def __getattr__(self, attr):
+        return self.entity(attr)
+
+    def __repr__(self):
+        return 'SQLSoup(%r)' % self._metadata
+

tests/__init__.py

+import re
+
+def eq_(a, b, msg=None):
+    """Assert a == b, with repr messaging on failure."""
+    assert a == b, msg or "%r != %r" % (a, b)
+
+def ne_(a, b, msg=None):
+    """Assert a != b, with repr messaging on failure."""
+    assert a != b, msg or "%r == %r" % (a, b)
+
+def assert_raises(except_cls, callable_, *args, **kw):
+    try:
+        callable_(*args, **kw)
+        success = False
+    except except_cls, e:
+        success = True
+
+    # assert outside the block so it works for AssertionError too !
+    assert success, "Callable did not raise an exception"
+
+def assert_raises_message(except_cls, msg, callable_, *args, **kwargs):
+    try:
+        callable_(*args, **kwargs)
+        assert False, "Callable did not raise an exception"
+    except except_cls, e:
+        assert re.search(msg, str(e)), "%r !~ %s" % (msg, e)
+        print str(e)

tests/test_sqlsoup.py

+import sqlsoup
+from tests import eq_, assert_raises, \
+    assert_raises_message
+from sqlalchemy import create_engine, or_, desc, select, func, exc, \
+    Table, util, Column, Integer
+from sqlalchemy.orm import scoped_session, sessionmaker
+import datetime
+import unittest
+
+class SQLSoupTest(unittest.TestCase):
+
+    @classmethod
+    def setup_class(cls):
+        global engine
+        engine = create_engine('sqlite://', echo=True)
+        for sql in _ddl:
+            engine.execute(sql)
+
+    @classmethod
+    def teardown_class(cls):
+        engine.dispose()
+
+    def setUp(self):
+        for sql in _data:
+            engine.execute(sql)
+
+    def tearDown(self):
+        sqlsoup.Session.remove()
+        for sql in _teardown:
+            engine.execute(sql)
+
+    def test_map_to_attr_present(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        users = db.users
+        assert_raises_message(
+            sqlsoup.SQLSoupError,
+            "Attribute 'users' is already mapped",
+            db.map_to, 'users', tablename='users'
+        )
+
+    def test_map_to_table_not_string(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer, primary_key=True))
+        assert_raises_message(
+            sqlsoup.ArgumentError,
+            "'tablename' argument must be a string.",
+            db.map_to, 'users', tablename=table
+        )
+
+    def test_map_to_table_or_selectable(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer, primary_key=True))
+        assert_raises_message(
+            sqlsoup.ArgumentError,
+            "'tablename' and 'selectable' arguments are mutually exclusive",
+            db.map_to, 'users', tablename='users', selectable=table
+        )
+
+    def test_map_to_no_pk_selectable(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer))
+        assert_raises_message(
+            sqlsoup.SQLSoupError,
+            "table 'users' does not have a primary ",
+            db.map_to, 'users', selectable=table
+        )
+    def test_map_to_invalid_schema(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer))
+        assert_raises_message(
+            sqlsoup.ArgumentError,
+            "'tablename' argument is required when "
+                                "using 'schema'.",
+            db.map_to, 'users', selectable=table, schema='hoho'
+        )
+    def test_map_to_nothing(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        assert_raises_message(
+            sqlsoup.ArgumentError,
+            "'tablename' or 'selectable' argument is "
+                                    "required.",
+            db.map_to, 'users', 
+        )
+
+    def test_map_to_string_not_selectable(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        assert_raises_message(
+            sqlsoup.ArgumentError,
+            "'selectable' argument must be a "
+                                    "table, select, join, or other "
+                                    "selectable construct.",
+            db.map_to, 'users', selectable='users'
+        )
+
+    def test_bad_names(self):
+        db = sqlsoup.SQLSoup(engine)
+
+        # print db.bad_names.c.id
+
+        print db.bad_names.c.query
+
+    def test_load(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedUsers = db.users
+        users = db.users.all()
+        users.sort()
+        eq_(users, [MappedUsers(name=u'Joe Student',
+            email=u'student@example.edu', password=u'student',
+            classname=None, admin=0),
+            MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1)])
+
+    def test_order_by(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedUsers = db.users
+        users = db.users.order_by(db.users.name).all()
+        eq_(users, [MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1), MappedUsers(name=u'Joe Student',
+            email=u'student@example.edu', password=u'student',
+            classname=None, admin=0)])
+
+    def test_whereclause(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedUsers = db.users
+        where = or_(db.users.name == 'Bhargan Basepair', db.users.email
+                    == 'student@example.edu')
+        users = \
+            db.users.filter(where).order_by(desc(db.users.name)).all()
+        eq_(users, [MappedUsers(name=u'Joe Student',
+            email=u'student@example.edu', password=u'student',
+            classname=None, admin=0),
+            MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1)])
+
+    def test_first(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedUsers = db.users
+        user = db.users.filter(db.users.name == 'Bhargan Basepair'
+                               ).one()
+        eq_(user, MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1))
+        db.rollback()
+        user = db.users.get('Bhargan Basepair')
+        eq_(user, MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1))
+        db.rollback()
+        user = db.users.filter_by(name='Bhargan Basepair').one()
+        eq_(user, MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1))
+        db.rollback()
+
+    def test_crud(self):
+
+        # note we're testing autoflush here too...
+
+        db = sqlsoup.SQLSoup(engine)
+        MappedLoans = db.loans
+        user = db.users.filter_by(name='Bhargan Basepair').one()
+        book_id = db.books.filter_by(title='Regional Variation in Moss'
+                ).first().id
+        loan_insert = db.loans.insert(book_id=book_id,
+                user_name=user.name)
+        loan = db.loans.filter_by(book_id=2,
+                                  user_name='Bhargan Basepair').one()
+        eq_(loan, loan_insert)
+        l2 = MappedLoans(book_id=2, user_name=u'Bhargan Basepair',
+                         loan_date=loan.loan_date)
+        eq_(loan, l2)
+        db.expunge(l2)
+        db.delete(loan)
+        loan = db.loans.filter_by(book_id=2,
+                                  user_name='Bhargan Basepair').first()
+        assert loan is None
+
+    def test_cls_crud(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedUsers = db.users
+        db.users.filter_by(name='Bhargan Basepair'
+                           ).update(dict(name='Some New Name'))
+        u1 = db.users.filter_by(name='Some New Name').one()
+        eq_(u1, MappedUsers(name=u'Some New Name',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1))
+
+    def test_map_table(self):
+        db = sqlsoup.SQLSoup(engine)
+        users = Table('users', db._metadata, autoload=True)
+        MappedUsers = db.map(users)
+        users = MappedUsers.order_by(db.users.name).all()
+        eq_(users, [MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1), MappedUsers(name=u'Joe Student',
+            email=u'student@example.edu', password=u'student',
+            classname=None, admin=0)])
+
+    def test_mapped_join(self):
+        db = sqlsoup.SQLSoup(engine)
+        join1 = MappedJoin = db.join(db.users, db.loans, isouter=True)
+        mj = join1.filter_by(name='Joe Student').all()
+        eq_(mj, [MappedJoin(
+            name=u'Joe Student',
+            email=u'student@example.edu',
+            password=u'student',
+            classname=None,
+            admin=0,
+            book_id=1,
+            user_name=u'Joe Student',
+            loan_date=datetime.datetime(2006, 7, 12, 0, 0),
+            )])
+        db.rollback()
+        join2 = MappedJoin = db.join(join1, db.books)
+        mj = join2.all()
+        eq_(mj, [MappedJoin(
+            name=u'Joe Student',
+            email=u'student@example.edu',
+            password=u'student',
+            classname=None,
+            admin=0,
+            book_id=1,
+            user_name=u'Joe Student',
+            loan_date=datetime.datetime(2006, 7, 12, 0, 0),
+            id=1,
+            title=u'Mustards I Have Known',
+            published_year=u'1989',
+            authors=u'Jones',
+            )])
+        eq_(db.with_labels(join1).c.keys(), [
+            u'users_name',
+            u'users_email',
+            u'users_password',
+            u'users_classname',
+            u'users_admin',
+            u'loans_book_id',
+            u'loans_user_name',
+            u'loans_loan_date',
+            ])
+        labeled_loans = db.with_labels(db.loans)
+        eq_(db.join(db.users, labeled_loans, isouter=True).c.keys(), [
+            u'name',
+            u'email',
+            u'password',
+            u'classname',
+            u'admin',
+            u'loans_book_id',
+            u'loans_user_name',
+            u'loans_loan_date',
+            ])
+
+    def test_relations(self):
+        db = sqlsoup.SQLSoup(engine)
+        db.users.relate('loans', db.loans)
+        MappedLoans = db.loans
+        MappedUsers = db.users
+        eq_(db.users.get('Joe Student').loans, [MappedLoans(book_id=1,
+            user_name=u'Joe Student', loan_date=datetime.datetime(2006,
+            7, 12, 0, 0))])
+        db.rollback()
+        eq_(db.users.filter(~db.users.loans.any()).all(),
+            [MappedUsers(name=u'Bhargan Basepair',
+            email='basepair@example.edu', password=u'basepair',
+            classname=None, admin=1)])
+        db.rollback()
+        del db._cache['users']
+        db.users.relate('loans', db.loans, order_by=db.loans.loan_date,
+                        cascade='all, delete-orphan')
+
+    def test_relate_m2o(self):
+        db = sqlsoup.SQLSoup(engine)
+        db.loans.relate('user', db.users)
+        u1 = db.users.filter(db.users.c.name=='Joe Student').one()
+        eq_(db.loans.first().user, u1)
+
+    def test_explicit_session(self):
+        Session = scoped_session(sessionmaker())
+        db = sqlsoup.SQLSoup(engine, session=Session)
+        try:
+            MappedUsers = db.users
+            sess = Session()
+            assert db.users._query.session is db.users.session is sess
+            row = db.users.insert(name='new name', email='new email')
+            assert row in sess
+        finally:
+            sess.rollback()
+            sess.close()
+
+    def test_selectable(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedBooks = db.books
+        b = db.books._table
+        s = select([b.c.published_year, func.count('*').label('n')],
+                   from_obj=[b], group_by=[b.c.published_year])
+        s = s.alias('years_with_count')
+        years_with_count = db.map(s, primary_key=[s.c.published_year])
+        eq_(years_with_count.filter_by(published_year='1989').all(),
+            [MappedBooks(published_year=u'1989', n=1)])
+
+    def test_raw_sql(self):
+        db = sqlsoup.SQLSoup(engine)
+        rp = db.execute('select name, email from users order by name')
+        eq_(rp.fetchall(), [('Bhargan Basepair', 'basepair@example.edu'
+            ), ('Joe Student', 'student@example.edu')])
+
+        # test that execute() shares the same transactional context as
+        # the session
+
+        db.execute("update users set email='foo bar'")
+        eq_(db.execute('select distinct email from users').fetchall(),
+            [('foo bar', )])
+        db.rollback()
+        eq_(db.execute('select distinct email from users').fetchall(),
+            [(u'basepair@example.edu', ), (u'student@example.edu', )])
+
+    def test_connection(self):
+        db = sqlsoup.SQLSoup(engine)
+        conn = db.connection()
+        rp = conn.execute('select name, email from users order by name')
+        eq_(rp.fetchall(), [('Bhargan Basepair', 'basepair@example.edu'
+            ), ('Joe Student', 'student@example.edu')])
+
+    def test_entity(self):
+        db = sqlsoup.SQLSoup(engine)
+        tablename = 'loans'
+        eq_(db.entity(tablename), db.loans)
+
+    def test_entity_with_different_base(self):
+        class subclass(object):
+            pass
+
+        db = sqlsoup.SQLSoup(engine, base=subclass)
+        assert issubclass(db.entity('loans'), subclass)
+
+    def test_filter_by_order_by(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedUsers = db.users
+        users = \
+            db.users.filter_by(classname=None).order_by(db.users.name).all()
+        eq_(users, [MappedUsers(name=u'Bhargan Basepair',
+            email=u'basepair@example.edu', password=u'basepair',
+            classname=None, admin=1), MappedUsers(name=u'Joe Student',
+            email=u'student@example.edu', password=u'student',
+            classname=None, admin=0)])
+
+    def test_no_pk_reflected(self):
+        db = sqlsoup.SQLSoup(engine)
+        assert_raises(sqlsoup.SQLSoupError, getattr, db, 'nopk')
+
+    def test_nosuchtable(self):
+        db = sqlsoup.SQLSoup(engine)
+        assert_raises(exc.NoSuchTableError, getattr, db, 'nosuchtable')
+
+    def test_dont_persist_alias(self):
+        db = sqlsoup.SQLSoup(engine)
+        MappedBooks = db.books
+        b = db.books._table
+        s = select([b.c.published_year, func.count('*').label('n')],
+                   from_obj=[b], group_by=[b.c.published_year])
+        s = s.alias('years_with_count')
+        years_with_count = db.map(s, primary_key=[s.c.published_year])
+        assert_raises(sqlsoup.SQLSoupError, years_with_count.insert,
+                      published_year='2007', n=1)
+
+    def test_clear(self):
+        db = sqlsoup.SQLSoup(engine)
+        eq_(db.loans.count(), 1)
+        _ = db.loans.insert(book_id=1, user_name='Bhargan Basepair')
+        db.expunge_all()
+        db.flush()
+        eq_(db.loans.count(), 1)
+
+
+_ddl = \
+    u"""
+CREATE TABLE books (
+    id                   integer PRIMARY KEY, -- auto-increments in sqlite
+    title                text NOT NULL,
+    published_year       char(4) NOT NULL,
+    authors              text NOT NULL
+);
+
+CREATE TABLE users (
+    name                 varchar(32) PRIMARY KEY,
+    email                varchar(128) NOT NULL,
+    password             varchar(128) NOT NULL,
+    classname            text,
+    admin                int NOT NULL -- 0 = false
+);
+
+CREATE TABLE loans (
+    book_id              int PRIMARY KEY REFERENCES books(id),
+    user_name            varchar(32) references users(name)
+        ON DELETE SET NULL ON UPDATE CASCADE,
+    loan_date            datetime DEFAULT current_timestamp
+);
+
+CREATE TABLE nopk (
+    i                    int
+);
+
+CREATE TABLE bad_names (
+   id int primary key,
+   query  varchar(100)
+)
+""".split(';'
+        )
+_data = \
+    """
+insert into users(name, email, password, admin)
+values('Bhargan Basepair', 'basepair@example.edu', 'basepair', 1);
+insert into users(name, email, password, admin)
+values('Joe Student', 'student@example.edu', 'student', 0);
+
+insert into books(title, published_year, authors)
+values('Mustards I Have Known', '1989', 'Jones');
+insert into books(title, published_year, authors)
+values('Regional Variation in Moss', '1971', 'Flim and Flam');
+
+insert into loans(book_id, user_name, loan_date)
+values (
+    (select min(id) from books),
+    (select name from users where name like 'Joe%'),
+    '2006-07-12 0:0:0')
+;
+""".split(';'
+        )
+_teardown = \
+    """
+delete from loans;
+delete from books;
+delete from users;
+delete from nopk;
+""".split(';'
+        )
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.