
SQLAlchemy Support
******************

Since most people prefer to use an ORM the crate client library
provides support for SQLAlchemy out of the box.

The Dialect is installed and registered by default (e.g. if crate was
installed using pip) and can be used without further configuration.


Connection String
=================

In SQLAlchemy a connection is established using the create_engine
function. This function takes a connection string that varies from
database to database.

In order to connect to a crate cluster the following connection
strings are valid:

   >>> sa.create_engine('crate://')
   Engine(crate://)

This will connect to the default server ('127.0.0.1:9200'). In order
to connect to a different server the following syntax can be used:

   >>> sa.create_engine('crate://otherserver:9200')
   Engine(crate://otherserver:9200)

Since Crate is a clustered database that usually consists of multiple
server it is recommended to connect to all of them. This enables the
DB-API layer to use round-robin to distribute the load and skip a
server if it becomes unavailable.

The connect_args parameter has to be used to do so:

   >>> sa.create_engine('crate://', connect_args={
   ...     'servers': ['host1:9200', 'host2:9200']
   ... })
   Engine(crate://)


Supported Operations and Limitations
------------------------------------

Currently Crate only implements a subset of the SQL standard.
Therefore many operations that SQLAlchemy provides are currently not
supported.

An overview of supported operations:

   - simple select statements including filter operations, limit and offset. But
     without joins or subselects. Order by is also supported.
   - Inserting new rows.

Unlike other databases that are usually used with SQLAlchemy, Crate
isn't a RDBMS but instead is document oriented with eventual
consistency.

This means that there is no transaction support and the database
should be modeled without relationships.

In SQLAlchemy a Session object is used to query the database. This
Session object contains a rollback method that in Crate won't do
anything at all. Similar the commit method will only flush but not
actually commit, as there is no commit in crate.

Please refer to the SQLAlchemy documentation for more information
about the session management and the concept of flush.


Complex Types
-------------

In a document oriented database it is a common pattern to store
complex objects within a single field. For such cases the crate
package provides the Craty type.

Below is a schema definition using SQLAlchemy's declarative approach:

   >>> from crate.client.sqlalchemy.types import Craty
   >>> from uuid import uuid4

   >>> def gen_key():
   ...     return str(uuid4())

   >>> class Character(Base):
   ...     __tablename__ = 'characters'
   ...     id = sa.Column(sa.String, primary_key=True, default=gen_key)
   ...     name = sa.Column(sa.String)
   ...     details = sa.Column(Craty)

Using the Session two characters are added that have additional
attributes inside the details column that weren't defined in the
schema:

   >>> arthur = Character(name='Arthur Dent')
   >>> arthur.details = {}
   >>> arthur.details['gender'] = 'male'
   >>> arthur.details['species'] = 'human'
   >>> session.add(arthur)

   >>> trillian = Character(name='Tricia McMillan')
   >>> trillian.details = {}
   >>> trillian.details['gender'] = 'female'
   >>> trillian.details['species'] = 'human'
   >>> trillian.details['female_only_attribute'] = 1
   >>> session.add(trillian)
   >>> session.commit()

After INSERT statements are sent to the database the newly inserted
rows aren't immediately available for search because the index is only
updated periodically:

   >>> from time import sleep
   >>> sleep(2)

Note: Newly inserted rows can still be queried immediately if a lookup by
  the primary key is done.

A regular select query will then fetch the whole documents:

   >>> query = session.query(Character).order_by(Character.name)
   >>> [(c.name, c.details['gender']) for c in query]
   [('Arthur Dent', 'male'), ('Tricia McMillan', 'female')]

But it is also possible to just select a part of the document, even
inside the Craty type:

   >>> sorted(session.query(Character.details['gender']).all())
   [('female',), ('male',)]

In addition, filtering on the attributes inside the details column is
also possible:

   >>> query = session.query(Character.name)
   >>> query.filter(Character.details['gender'] == 'male').all()
   [('Arthur Dent',)]
