
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',)]
