======
Cursor
======

::

    >>> from crate.client import connect

    >>> connection = connect(client=connection_client_mocked)
    >>> cursor = connection.cursor()

The rowcount and duration attribute is -1 in case no ``execute()`` has been performed on the cursor.

::

    >>> cursor.rowcount
    -1

    >>> cursor.duration
    -1

Hardcode the next response of the mocked connection client, so we won't need a sql statement
to execute::

    >>> connection.client.set_next_response({
    ...     "rows":[ [ "North West Ripple", 1 ], [ "Arkintoofle Minor", 3 ], [ "Alpha Centauri", 3 ] ],
    ...     "cols":[ "name", "position" ],
    ...     "rowcount":3,
    ...     "duration":123
    ... })

fetchone()
==========

Calling ``fetchone()`` on the cursor object the first time after an execute returns the first row::

    >>> cursor.execute('')

    >>> cursor.fetchone()
    ['North West Ripple', 1]

Each call to ``fetchone()`` increments the cursor and returns the next row::

    >>> cursor.fetchone()
    ['Arkintoofle Minor', 3]

One more iteration::

    >>> cursor.next()
    ['Alpha Centauri', 3]

The iteration is stopped after the last row is returned.
A further call to ``fetchone()`` returns an empty result::

    >>> cursor.fetchone()

Using ``fetchone()`` on a cursor before issuing a database statement results
in an error::

    >>> new_cursor = connection.cursor()
    >>> new_cursor.fetchone()
    Traceback (most recent call last):
    ...
    crate.client.exceptions.ProgrammingError: No result available. execute() or executemany() must be called first.


fetchmany()
===========

``fetchmany()`` takes an argument which specifies the number of rows we want to fetch::

    >>> cursor.execute('')

    >>> cursor.fetchmany(2)
    [['North West Ripple', 1], ['Arkintoofle Minor', 3]]

If the specified number of rows not being available, fewer rows may returned::

    >>> cursor.fetchmany(2)
    [['Alpha Centauri', 3]]

    >>> cursor.execute('')

If no number of rows are specified it defaults to the current cursor.arraysize::

    >>> cursor.arraysize
    1

    >>> cursor.fetchmany()
    [['North West Ripple', 1]]

    >>> cursor.execute('')
    >>> cursor.arraysize = 2
    >>> cursor.fetchmany()
    [['North West Ripple', 1], ['Arkintoofle Minor', 3]]

If zero number of rows are specified, all rows left are returned::

    >>> cursor.fetchmany(0)
    [['Alpha Centauri', 3]]

fetchall()
==========

``fetchall()`` fetches all (remaining) rows of a query result::

    >>> cursor.execute('')

    >>> cursor.fetchall()
    [['North West Ripple', 1], ['Arkintoofle Minor', 3], ['Alpha Centauri', 3]]

Since all data was fetched 'None' is returned by ``fetchone()``::

    >>> cursor.fetchone()

And each other call returns an empty sequence::

    >>> cursor.fetchmany(2)
    []

    >>> cursor.fetchmany()
    []

    >>> cursor.fetchall()
    []

iteration
=========

The cursor supports the iterator interface and can be iterated upon::

    >>> cursor.execute('')
    >>> [row for row in cursor]
    [['North West Ripple', 1], ['Arkintoofle Minor', 3], ['Alpha Centauri', 3]]

When no other call to execute has been done, it will raise StopIteration on
subsequent iterations::

    >>> next(cursor)
    Traceback (most recent call last):
    ...
    StopIteration

    >>> cursor.execute('')
    >>> for row in cursor:
    ...     row
    ['North West Ripple', 1]
    ['Arkintoofle Minor', 3]
    ['Alpha Centauri', 3]

Iterating over a new cursor without results will immediately raise a ProgrammingError::

    >>> new_cursor = connection.cursor()
    >>> next(new_cursor)
    Traceback (most recent call last):
    ...
    crate.client.exceptions.ProgrammingError: No result available. execute() or executemany() must be called first.

description
===========

::

    >>> cursor.description
    (('name', None, None, None, None, None, None), ('position', None, None, None, None, None, None))

rowcount
========

The ``rowcount`` property specifies the number of rows that the last ``execute()`` produced::

    >>> cursor.execute('')
    >>> cursor.rowcount
    3

The attribute is -1 in case the cursor has been closed::

    >>> cursor.close()
    >>> cursor.rowcount
    -1

If the last respsonse doesn't contain the rowcount attribute -1 is returned::

    >>> cursor = connection.cursor()
    >>> connection.client.set_next_response({
    ...     "rows":[],
    ...     "cols":[],
    ...     "duration":123
    ... })

    >>> cursor.execute('')
    >>> cursor.rowcount
    -1

    >>> connection.client.set_next_response({
    ...     "rows":[ [ "North West Ripple", 1 ], [ "Arkintoofle Minor", 3 ], [ "Alpha Centauri", 3 ] ],
    ...     "cols":[ "name", "position" ],
    ...     "rowcount":3,
    ...     "duration":123
    ... })

duration
========

The ``duration`` property specifies the server-side duration in milliseconds of the last query
issued by ``execute()``::

    >>> cursor = connection.cursor()
    >>> cursor.execute('')
    >>> cursor.duration
    123

The attribute is -1 in case the cursor has been closed::

    >>> cursor.close()
    >>> cursor.duration
    -1

    >>> connection.client.set_next_response({
    ...     "results": [
    ...         {
    ...             "rowcount": 3
    ...         },
    ...         {
    ...             "rowcount": 2
    ...         }
    ...     ],
    ...     "duration":123,
    ...     "cols":[ "name", "position" ],
    ... })

executemany
===========

``executemany()`` allows to execute a single sql statement against a sequence
of parameters::

    >>> cursor = connection.cursor()

    >>> cursor.executemany('', (1,2,3))
    [{'rowcount': 3}, {'rowcount': 2}]

    >>> cursor.rowcount
    5
    >>> cursor.duration
    123

``executemany()`` is not intended to be used with statements returning result
sets. The result will alway be empty::

    >>> cursor.fetchall()
    []

For completeness' sake the cursor description is updated nonetheless::

    >>> [ desc[0] for desc in cursor.description ]
    ['name', 'position']

    >>> connection.client.set_next_response({
    ...     "rows":[ [ "North West Ripple", 1 ], [ "Arkintoofle Minor", 3 ], [ "Alpha Centauri", 3 ] ],
    ...     "cols":[ "name", "position" ],
    ...     "rowcount":3,
    ...     "duration":123
    ... })


Usually ``executemany`` sends the ``bulk_args`` parameter to the crate sql 
endpoint which was introduced with Crate 0.42.0.
If the client is connected to at least one crate server with an older version
the client executes the statement once for every parameter within the sequence.

The resulting ``rowcount`` and ``duration`` are the sum of all calls made to
the server during this method execution.::

    >>> connection_client_mocked.set_next_server_infos("local:4200", "my crate", "0.41.9")
    >>> connection = connect(client=connection_client_mocked)
    >>> cursor = connection.cursor()
    >>> cursor.executemany('', (1,2,3))
    >>> cursor.rowcount
    9
    >>> cursor.duration
    369

close()
=======

After closing a cursor the connection will be unusable. If any operation is attempted with the
closed connection an ``ProgrammingError`` exception will be raised::

    >>> cursor = connection.cursor()
    >>> cursor.execute('')
    >>> cursor.fetchone()
    ['North West Ripple', 1]

    >>> cursor.close()
    >>> cursor.fetchone()
    Traceback (most recent call last):
    ...
    crate.client.exceptions.ProgrammingError: Cursor closed

    >>> cursor.fetchmany()
    Traceback (most recent call last):
    ...
    crate.client.exceptions.ProgrammingError: Cursor closed

    >>> cursor.fetchall()
    Traceback (most recent call last):
    ...
    crate.client.exceptions.ProgrammingError: Cursor closed

    >>> cursor.next()
    Traceback (most recent call last):
    ...
    crate.client.exceptions.ProgrammingError: Cursor closed

