Skip to content

Commit

Permalink
Refactored docs about using database, added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vmagamedov committed Sep 14, 2016
1 parent 9d7d763 commit 81a0f7b
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 185 deletions.
163 changes: 163 additions & 0 deletions docs/guide/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# setup storage

from sqlalchemy import create_engine, MetaData, Table, Column
from sqlalchemy import Integer, String, ForeignKey

metadata = MetaData()

character_table = Table(
'character',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('species', String),
)

actor_table = Table(
'actor',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('character_id', ForeignKey('character.id'), nullable=False),
)

sa_engine = create_engine('sqlite://')
metadata.create_all(sa_engine)

sa_engine.execute(character_table.insert().values([
dict(id=1, name='James T. Kirk', species='Human'),
dict(id=2, name='Spock', species='Vulcan/Human'),
dict(id=3, name='Leonard McCoy', species='Human'),
]))
sa_engine.execute(actor_table.insert().values([
dict(id=1, character_id=1, name='William Shatner'),
dict(id=2, character_id=2, name='Leonard Nimoy'),
dict(id=3, character_id=3, name='DeForest Kelley'),
dict(id=4, character_id=1, name='Chris Pine'),
dict(id=5, character_id=2, name='Zachary Quinto'),
dict(id=6, character_id=3, name='Karl Urban'),
]))

# define graph

from hiku.graph import Graph, Root, Edge, Link, One, Many
from hiku.engine import pass_context
from hiku.sources import sqlalchemy as sa

SA_ENGINE = 'sa-engine'

character_query = sa.FieldsQuery(SA_ENGINE, character_table)

actor_query = sa.FieldsQuery(SA_ENGINE, actor_table)

character_to_actors_query = sa.LinkQuery(Many, SA_ENGINE, edge='actor',
from_column=actor_table.c.character_id,
to_column=actor_table.c.id)

def direct_link(ids):
return ids

@pass_context
def to_characters_query(ctx):
query = character_table.select(character_table.c.id)
return [row.id for row in ctx[SA_ENGINE].execute(query)]

@pass_context
def to_actors_query(ctx):
query = actor_table.select(actor_table.c.id)
return [row.id for row in ctx[SA_ENGINE].execute(query)]

GRAPH = Graph([
Edge('character', [
sa.Field('id', character_query),
sa.Field('name', character_query),
sa.Field('species', character_query),
sa.Link('actors', character_to_actors_query, requires='id'),
]),
Edge('actor', [
sa.Field('id', actor_query),
sa.Field('name', actor_query),
sa.Field('character_id', actor_query),
Link('character', One, direct_link,
edge='character', requires='character_id'),
]),
Root([
Link('characters', Many, to_characters_query,
edge='character', requires=None),
Link('actors', Many, to_actors_query,
edge='actor', requires=None),
]),
])

# test graph

from hiku.engine import Engine
from hiku.result import denormalize
from hiku.readers.simple import read
from hiku.executors.sync import SyncExecutor

hiku_engine = Engine(SyncExecutor())

def execute(graph, query_string):
query = read(query_string)
result = hiku_engine.execute(graph, query, {SA_ENGINE: sa_engine})
return denormalize(graph, result, query)

def test_character_to_actors():
result = execute(GRAPH, '[{:characters [:name {:actors [:name]}]}]')
assert result == {
'characters': [
{
'name': 'James T. Kirk',
'actors': [
{'name': 'William Shatner'},
{'name': 'Chris Pine'},
],
},
{
'name': 'Spock',
'actors': [
{'name': 'Leonard Nimoy'},
{'name': 'Zachary Quinto'},
],
},
{
'name': 'Leonard McCoy',
'actors': [
{'name': 'DeForest Kelley'},
{'name': 'Karl Urban'},
],
},
],
}

def test_actor_to_character():
result = execute(GRAPH, '[{:actors [:name {:character [:name]}]}]')
assert result == {
'actors': [
{
'name': 'William Shatner',
'character': {'name': 'James T. Kirk'},
},
{
'name': 'Leonard Nimoy',
'character': {'name': 'Spock'},
},
{
'name': 'DeForest Kelley',
'character': {'name': 'Leonard McCoy'},
},
{
'name': 'Chris Pine',
'character': {'name': 'James T. Kirk'},
},
{
'name': 'Zachary Quinto',
'character': {'name': 'Spock'},
},
{
'name': 'Karl Urban',
'character': {'name': 'Leonard McCoy'},
},
],
}
178 changes: 44 additions & 134 deletions docs/guide/database.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Using databases
===============
Using database
==============

Hiku provides support for loading data from SQL databases using SQLAlchemy_
library, but Hiku doesn't requires to use it's ORM layer, it requires only Core
Expand All @@ -9,44 +9,11 @@ construct SELECT queries.
Prerequisites
~~~~~~~~~~~~~

We will translate our previous example from the :doc:`introduction`, here is it's
database schema:
We will translate our previous example from the :doc:`introduction`, but now all
the data is stored in the SQLite database:

.. code-block:: python
from sqlalchemy import MetaData, Table, Column, Integer, String, ForeignKey
metadata = MetaData()
character_table = Table(
'character',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('species', String),
)
actor_table = Table(
'actor',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String),
Column('character_id', ForeignKey('character.id'), nullable=False),
)
And let's store the same :ref:`data <introduction-data>` in our database:

.. code-block:: python
from sqlalchemy import create_engine
engine = create_engine('sqlite://')
metadata.create_all(engine)
for character_data in data['character'].values():
engine.execute(character_table.insert().values(character_data))
for actor_data in data['actor'].values():
engine.execute(actor_table.insert().values(actor_data))
.. literalinclude:: database.py
:lines: 3-39

Graph definition
~~~~~~~~~~~~~~~~
Expand All @@ -55,66 +22,27 @@ Defined tables can be exposed as graph of edges:

.. _guide-database-graph:

.. code-block:: python
from hiku.graph import Graph, Root, Edge, Link, One, Many
from hiku.engine import pass_context
from hiku.sources import sqlalchemy as sa
SA_ENGINE = 'sa-engine' # 1
character_query = sa.FieldsQuery(SA_ENGINE, character_table) # 2
actor_query = sa.FieldsQuery(SA_ENGINE, actor_table)
to_actors_query = sa.LinkQuery(Many, SA_ENGINE, edge='actor', # 3
from_column=actor_table.c.character_id,
to_column=actor_table.c.id)
def to_character_func(ids): # 4
return ids
@pass_context # 5
def to_characters_query(ctx): # 6
query = character_table.select(character_table.c.id)
return [row.id for row in ctx[SA_ENGINE].execute(query)] # 7
GRAPH = Graph([
Edge('character', [
sa.Field('id', character_query),
sa.Field('name', character_query),
sa.Field('species', character_query),
sa.Link('actors', to_actors_query, requires='id'),
]),
Edge('actor', [
sa.Field('id', actor_query),
sa.Field('name', actor_query),
sa.Field('character_id', actor_query),
Link('character', One, to_character_func, # 8
edge='character', requires='character_id'),
]),
Root([
Link('characters', Many, to_characters_query, # 9
edge='character', requires=None),
]),
])
.. literalinclude:: database.py
:lines: 43-90
:linenos:
:emphasize-lines: 5,7,11-13,15,18,19,21,39-40

In the previous examples all the data was available as data structures, so no
special access method was required. With databases we will require a database
connection in order to fetch any data from it. Hiku provides simple and
implicit way to solve this issue without using global variables (thread-locals)
- by providing query execution context.
connection in order to fetch any data from it. Hiku provides simple and implicit
way to solve this issue without using global variables (thread-locals) - by
providing query execution context.

Query execution context is a simple mapping, where you can store and read values
during query execution. In this example we are using ``SA_ENGINE``
constant :sup:`[1]` as a key to access our SQLAlchemy's engine. In order to
access query context :py:func:`~hiku.engine.pass_context` decorator should
be used :sup:`[5]` and then ``to_characters_query`` function :sup:`[6]` will
receive it as a first positional argument. ``SA_ENGINE`` constant is used to get
SQLAlchemy's engine from the context :sup:`[7]` in order to execute SQL query.

:py:class:`~hiku.sources.sqlalchemy.FieldsQuery` :sup:`[2]` and
:py:class:`~hiku.sources.sqlalchemy.LinkQuery` :sup:`[3]` are using context
during query execution. In this example we are using ``SA_ENGINE`` constant
:sup:`[18]` as a key to access our SQLAlchemy's engine. In order to access query
context :py:func:`~hiku.engine.pass_context` decorator should be used
:sup:`[18]` and then ``to_characters_query`` function :sup:`[19]` will receive
it as a first positional argument. ``SA_ENGINE`` constant is used to get
SQLAlchemy's engine from the context :sup:`[21]` in order to execute SQL query.

:py:class:`~hiku.sources.sqlalchemy.FieldsQuery` :sup:`[7]` and
:py:class:`~hiku.sources.sqlalchemy.LinkQuery` :sup:`[11-13]` are using context
in the same manner.

Hiku's SQLAlchemy support is provided by
Expand All @@ -124,14 +52,13 @@ the edge. And by :py:class:`hiku.sources.sqlalchemy.LinkQuery` and
:py:class:`hiku.sources.sqlalchemy.Link` to express relations between tables as
links between edges.

``to_character_func`` :sup:`[4]` is a special case: when one table contains
foreign key to the other table - `many-to-one` relation or `one-to-one`
relation, no additional queries needed to make a direct link between those
tables as edges. ``character`` link :sup:`[8]` is a good example of such direct
link.
``direct_link`` :sup:`[15]` is a special case: when one table contains foreign
key to the other table - `many-to-one` relation or `one-to-one` relation, no
additional queries needed to make a direct link between those tables as edges.
``character`` link :sup:`[39-40]` is a good example of such direct link.

Other relation types require to make additional query in order to fetch
linked edge ids. ``to_actors_query`` :sup:`[3]` for example. Such queries require
Other relation types require to make additional query in order to fetch linked
edge ids. ``to_actors_query`` :sup:`[11-13]` for example. Such queries require
selecting only one table, ``actor_table`` in this example. SQL query will be
looking like this:

Expand All @@ -148,38 +75,21 @@ edge), all we need is to fetch ``actor.id`` column to make a link from
Querying graph
~~~~~~~~~~~~~~

.. code-block:: python
from pprint import pprint
from hiku.engine import Engine
from hiku.result import denormalize
from hiku.readers.simple import read
from hiku.executors.sync import SyncExecutor
engine = Engine(SyncExecutor())
query = read('[{:characters [:name {:actors [:name {:character [:name]}]}]}]')
result = engine.execute(GRAPH, query, ctx={SA_ENGINE: sa_engine})
Result:

.. code-block:: python
>>> pprint(denormalize(GRAPH, result, query))
{'characters': [{'actors': [{'character': {'name': 'James T. Kirk'},
'name': 'William Shatner'},
{'character': {'name': 'James T. Kirk'},
'name': 'Chris Pine'}],
'name': 'James T. Kirk'},
{'actors': [{'character': {'name': 'Spock'},
'name': 'Leonard Nimoy'},
{'character': {'name': 'Spock'},
'name': 'Zachary Quinto'}],
'name': 'Spock'},
{'actors': [{'character': {'name': 'Leonard McCoy'},
'name': 'DeForest Kelley'},
{'character': {'name': 'Leonard McCoy'},
'name': 'Karl Urban'}],
'name': 'Leonard McCoy'}]}
For testing purposes let's define helper function ``execute``:

.. literalinclude:: database.py
:lines: 94-104

Testing one to many link:

.. literalinclude:: database.py
:lines: 107-132
:dedent: 4

Testing many to one link:

.. literalinclude:: database.py
:lines: 135-163
:dedent: 4

.. _SQLAlchemy: http://www.sqlalchemy.org

0 comments on commit 81a0f7b

Please sign in to comment.