aiovertica 0.0.5
Small asynchronous driver for Vertica based on vertica_python
License: Apache-2.0
Requires Python: >=3.8,<3.10

# aiovertica
Small asynchronous driver for Vertica based on [vertica_python](https://github.com/vertica/vertica-python/).

## WARNING

This project was written "in haste" literally overnight, so it may not be stable.
Of course, most of the mechanisms are identical to the "project", but in any case, 
tests have not yet been written to be sure of this at least to some extent.

At the moment, the project has rewritten the mechanisms responsible for working 
with the network from a synchronous model to an asynchronous one, as well as 
in the `Connection` and` Cursor` modules, the synchronous methods have been replaced 
by asynchronous ones (where necessary), the code formatting has been slightly improved 
and type annotations have been added (but not always correct).

### Foreword
I would really like to see a ready-made library, ideally the same as `asyncpg`, 
but unfortunately it is not there. I would like to express my deep gratitude to the team 
that worked on the `vertica_python` for writing at least a synchronous driver - this greatly 
simplified the work. I have tried to keep all the licenses and the list of authors from 
the original project.

I would also be glad if the guys from `vertica_python` added something similar to the 
official driver and we would have a simple opportunity to work with the driver in 
asynchronous mode as well.

Perhaps in the future I will create a PR in the official driver (I have several 
proposals for the architecture), but at the moment it would take too long to implement 
the "correct" embedding into the existing implementation - the code was needed "yesterday".

I will be glad for any help in the development of the project. The `Issues` and` PRs` 
sections are looking forward to seeing you :).


## Installation

To install vertica-python with pip:
```bash
# Latest release version
pip install aiovertica

# Latest commit on master branch
pip install git+https://github.com/i8enn/aiovertica.git@master
```

To install vertica-python from source, run the following command from the root directory:

    python setup.py install

Source code for vertica-python can be found at:

    https://github.com/i8enn/aiovertica

#### Using Kerberos authentication

> #### WARNING: `Kerberos` not supported in currently version

vertica-python has optional Kerberos authentication support for Unix-like systems, which requires you to install the [kerberos](https://pypi.org/project/kerberos/) package:

    pip install kerberos

Note that `kerberos` is a python extension module, which means you need to install `python-dev`. The command depends on the package manager and will look like

    sudo [yum|apt-get|etc] install python-dev

## Usage


**Create connection**

```python
import aiovertica

conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             # autogenerated session label by default,
             'session_label': 'some_label',
             # default throw error on invalid UTF-8 results
             'unicode_error': 'strict',
             # SSL is disabled by default
             'ssl': False,
             # autocommit is off by default
             'autocommit': True,
             # using server-side prepared statements is disabled by default
             'use_prepared_statements': False,
             # connection timeout is not enabled by default
             # 5 seconds timeout for a socket operation (Establishing a TCP connection or read/write operation)
             'connection_timeout': 5}

# simple connection, with manual close
try:
    connection = await aiovertica.connect(**conn_info)
    # do things
finally:
    await connection.close()

# using with for auto connection closing after usage
async with vertica_python.connect(**conn_info) as connection:
    # do things
```

You can pass an `ssl.SSLContext` to `ssl` to customize the SSL connection options. For example,

```python
import aiovertica
import ssl

ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.check_hostname = True
ssl_context.load_verify_locations(cafile='/path/to/ca_file.pem')

conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             'ssl': ssl_context}
connection = await aiovertica.connect(**conn_info)

```

See more on SSL options [here](https://docs.python.org/3/library/ssl.html).

> #### WARNING: `Kerberos` not supported in currently version

In order to use Kerberos authentication, install [dependencies](#using-kerberos-authentication) first, and it is the user's responsibility to ensure that an Ticket-Granting Ticket (TGT) is available and valid. Whether a TGT is available can be easily determined by running the `klist` command. If no TGT is available, then it first must be obtained by running the `kinit` command or by logging in. You can pass in optional arguments to customize the authentication. The arguments are `kerberos_service_name`, which defaults to "vertica", and `kerberos_host_name`, which defaults to the value of argument `host`. For example,

```python
import aiovertica

conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             # The service name portion of the Vertica Kerberos principal
             'kerberos_service_name': 'vertica_krb',
             # The instance or host name portion of the Vertica Kerberos principal
             'kerberos_host_name': 'vcluster.example.com'}

async with aiovertica.connect(**conn_info) as conn:
    # do things
```

Logging is disabled by default if you do not pass values to both ```log_level``` and ```log_path```.  The default value of ```log_level``` is logging.WARNING. You can find all levels [here](https://docs.python.org/3/library/logging.html#logging-levels). The default value of ```log_path``` is 'vertica_python.log', the log file will be in the current execution directory. If ```log_path``` is set to ```''``` (empty string) or ```None```, no file handler is set, logs will be processed by root handlers. For example,

```python
import aiovertica
import logging

## Example 1: write DEBUG level logs to './vertica_python.log'
conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             'log_level': logging.DEBUG}
async with aiovertica.connect(**conn_info) as connection:
    # do things

## Example 2: write WARNING level logs to './path/to/logs/client.log'
conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             'log_path': 'path/to/logs/client.log'}
async with aiovertica.connect(**conn_info) as connection:
   # do things

## Example 3: write INFO level logs to '/home/admin/logs/vClient.log'
conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             'log_level': logging.INFO,
             'log_path': '/home/admin/logs/vClient.log'}
async with aiovertica.connect(**conn_info) as connection:
   # do things

## Example 4: use root handlers to process logs by setting 'log_path' to '' (empty string) 
conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             'log_level': logging.DEBUG,
             'log_path': ''}
async with aiovertica.connect(**conn_info) as connection:
    # do things
```

Connection Failover: Supply a list of backup hosts to ```backup_server_node``` for the client to try if the primary host you specify in the connection parameters (```host```, ```port```) is unreachable. Each item in the list should be either a host string (using default port 5433) or a (host, port) tuple. A host can be a host name or an IP address.

```python
import aiovertica

conn_info = {'host': 'unreachable.server.com',
             'port': 888,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             'backup_server_node': ['123.456.789.123', 'invalid.com', ('10.20.82.77', 6000)]}
connection = await aiovertica.connect(**conn_info)
```

Connection Load Balancing helps automatically spread the overhead caused by client connections across the cluster by having hosts redirect client connections to other hosts. Both the server and the client need to enable load balancing for it to function. If the server disables connection load balancing, the load balancing request from client will be ignored.

```python
import aiovertica

conn_info = {'host': '127.0.0.1',
             'port': 5433,
             'user': 'some_user',
             'password': 'some_password',
             'database': 'vdb',
             'connection_load_balance': True}

# Server enables load balancing
async with aiovertica.connect(**conn_info) as conn:
    cur = conn.cursor()
    await cur.execute("SELECT NODE_NAME FROM V_MONITOR.CURRENT_SESSION")
    result = await cur.fetchone()
    print("Client connects to primary node:", result[0])
    await cur.execute("SELECT SET_LOAD_BALANCE_POLICY('ROUNDROBIN')")

async with aiovertica.connect(**conn_info) as conn:
    cur = conn.cursor()
    await cur.execute("SELECT NODE_NAME FROM V_MONITOR.CURRENT_SESSION")
    result = await cur.fetchone()
    print("Client redirects to node:", result[0])

## Output
#  Client connects to primary node: v_vdb_node0003
#  Client redirects to node: v_vdb_node0005
```

Another way to set connection properties is passing a connection string to the keyword parameter `dsn` of `vertica_python.connect(dsn='...', **kwargs)`. The connection string is of the form:
```
vertica://(user):(password)@(host):(port)/(database)?(arg1=val1&arg2=val2&...)
```
The connection string would be parsed by `vertica_python.parse_dsn(connection_str)`, and the parsing result (a dictionary of keywords and values) would be merged with _kwargs_. If the same keyword is specified in both the sources, the _kwargs_ value overrides the parsed _dsn_ value. The `(arg1=val1&arg2=val2&...)` section can handle string/numeric/boolean values, blank and invalid value would be ignored.

```python
import aiovertica

connection_str = (
    'vertica://admin@localhost:5433/db1?connection_load_balance=True&connection_timeout=1.5&session_label=vpclient+123%7E456'
)
print(aiovertica.parse_dsn(connection_str))
# {'user': 'admin', 'host': 'localhost', 'port': 5433, 'database': 'db1',
#  'connection_load_balance': True, 'connection_timeout': 1.5, 'session_label': 'vpclient 123~456'}

additional_info = {
    'password': 'some_password', 
    'backup_server_node': ['10.6.7.123', ('10.20.82.77', 6000)]  # invalid value to be set in a connection string
}

async with aiovertica.connect(dsn=connection_str, **additional_info) as conn:
   # do things
```


**Stream query results**:

```python
cur = connection.cursor()
await cur.execute("SELECT * FROM a_table LIMIT 2")

async for row in cur.iterate():
    print(row)
# [ 1, 'some text', datetime.datetime(2014, 5, 18, 6, 47, 1, 928014) ]
# [ 2, 'something else', None ]

```
Streaming is recommended if you want to further process each row, save the results in a non-list/dict format (e.g. Pandas DataFrame), or save the results in a file.


**In-memory results as list**:

```python
cur = connection.cursor()
await cur.execute("SELECT * FROM a_table LIMIT 2")
await cur.fetchall()
# [ [1, 'something'], [2, 'something_else'] ]
```


**In-memory results as dictionary**:

```python
cur = connection.cursor('dict')
await cur.execute("SELECT * FROM a_table LIMIT 2")
await cur.fetchall()
# [ {'id': 1, 'value': 'something'}, {'id': 2, 'value': 'something_else'} ]
await connection.close()
```


**Query using named parameters or format parameters**:

vertica-python can automatically convert Python objects to SQL literals: using this feature your code will be more robust and reliable to prevent SQL injection attacks.

Prerequisites: Only SQL literals (i.e. query values) should be bound via these methods: they shouldn’t be used to merge table or field names to the query (_vertica-python_ will try quoting the table name as a string value, generating invalid SQL as it is actually a SQL Identifier). If you need to generate dynamically SQL queries (for instance choosing dynamically a table name) you have to construct the full query yourself.

Variables can be specified with named (__:name__) placeholders.
```python
cur = connection.cursor()
data = {'propA': 1, 'propB': 'stringValue'}
await cur.execute("SELECT * FROM a_table WHERE a = :propA AND b = :propB", data)
# converted into a SQL command similar to: "SELECT * FROM a_table WHERE a = 1 AND b = 'stringValue'"

await cur.fetchall()
# [ [1, 'stringValue'] ]
```

Variables can also be specified with positional format (__%s__) placeholders. The placeholder __must always be a %s__, even if a different placeholder (such as a %d for integers or %f for floats) may look more appropriate. __Never__ use Python string concatenation (+) or string parameters interpolation (%) to pass variables to a SQL query string.
```python
cur = connection.cursor()
data = (1, "O'Reilly")
await cur.execute("SELECT * FROM a_table WHERE a = %s AND b = %s" % data) # WRONG: % operator
await cur.execute("SELECT * FROM a_table WHERE a = %d AND b = %s", data)  # WRONG: %d placeholder
await cur.execute("SELECT * FROM a_table WHERE a = %s AND b = %s", data)  # correct
# converted into a SQL command similar to: "SELECT * FROM a_table WHERE a = 1 AND b = 'O''Reilly'"

await cur.fetchall()
# [ [1, "O'Reilly"] ]
```

The placeholder must not be quoted. _vertica-python_ will add quotes where needed.
```python
cur = connection.cursor()
await cur.execute("INSERT INTO table VALUES (':propA')", {'propA': "someString"}) # WRONG
await cur.execute("INSERT INTO table VALUES (:propA)", {'propA': "someString"})   # correct
await cur.execute("INSERT INTO table VALUES ('%s')", ("someString",)) # WRONG
await cur.execute("INSERT INTO table VALUES (%s)", ("someString",))   # correct
```

_vertica-python_ supports default mapping for many standard Python types. It is possible to adapt new Python types to SQL literals via `Cursor.register_sql_literal_adapter(py_class_or_type, adapter_function)` function. Example:
```python
class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Adapter should return a string value
def adapt_point(point):
    return "STV_GeometryPoint({},{})".format(point.x, point.y)

cur = conn.cursor()
cur.register_sql_literal_adapter(Point, adapt_point)

await cur.execute("INSERT INTO geom_data (geom) VALUES (%s)", [Point(1.23, 4.56)])
await cur.execute("select ST_asText(geom) from geom_data")
await cur.fetchall()
# [['POINT (1.23 4.56)']]
```

To help you debug the binding process during Cursor.execute*(), `Cursor.object_to_sql_literal(py_object)` function can be used to inspect the SQL literal string converted from a Python object.
```python
cur = conn.cursor
cur.object_to_sql_literal("O'Reilly")  # "'O''Reilly'"
cur.object_to_sql_literal(None)  # "NULL"
cur.object_to_sql_literal(True)  # "True"
cur.object_to_sql_literal(Decimal("10.00000"))  # "10.00000"
cur.object_to_sql_literal(datetime.date(2018, 9, 7))  # "'2018-09-07'"
cur.object_to_sql_literal(Point(-71.13, 42.36))  # "STV_GeometryPoint(-71.13,42.36)" if you registered in previous step
```


**Query using server-side prepared statements**:

Vertica server-side prepared statements let you define a statement once and then run it many times with different parameters. Placeholders in the statement are represented by question marks (?). Server-side prepared statements are useful for preventing SQL injection attacks.

```python
import aiovertica

# Enable using server-side prepared statements at connection level
conn_info = {
    'host': '127.0.0.1',
    'user': 'some_user',
    'password': 'some_password',
    'database': 'a_database',
    'use_prepared_statements': True,
}

async with aiovertica.connect(**conn_info) as connection:
    cur = connection.cursor()
    await cur.execute("CREATE TABLE tbl (a INT, b VARCHAR)")
    await cur.execute("INSERT INTO tbl VALUES (?, ?)", [1, 'aa'])
    await cur.execute("INSERT INTO tbl VALUES (?, ?)", [2, 'bb'])
    await cur.executemany("INSERT INTO tbl VALUES (?, ?)", [(3, 'foo'), (4, 'xx'), (5, 'bar')])
    await cur.execute("COMMIT")

    await cur.execute("SELECT * FROM tbl WHERE a>=? AND a<=? ORDER BY a", (2,4))
    await cur.fetchall()
    # [[2, 'bb'], [3, 'foo'], [4, 'xx']]
```

Vertica does not support executing a command string containing multiple statements using server-side prepared statements. You can set ```use_prepared_statements``` option in ```cursor.execute*()``` functions to override the connection level setting.

```python
import aiovertica

# Enable using server-side prepared statements at connection level
conn_info = {
    'host': '127.0.0.1',
    'user': 'some_user',
    'password': 'some_password',
    'database': 'a_database',
    'use_prepared_statements': True,
}

async with aiovertica.connect(**conn_info) as connection:
    cur = connection.cursor()
    await cur.execute("CREATE TABLE tbl (a INT, b VARCHAR)")

    # Executing compound statements
    await cur.execute("INSERT INTO tbl VALUES (?, ?); COMMIT", [1, 'aa'])
    # Error message: Cannot insert multiple commands into a prepared statement

    # Disable prepared statements but forget to change placeholders (?)
    await cur.execute("INSERT INTO tbl VALUES (?, ?); COMMIT;", [1, 'aa'], use_prepared_statements=False)
    # TypeError: not all arguments converted during string formatting

    await cur.execute("INSERT INTO tbl VALUES (%s, %s); COMMIT;", [1, 'aa'], use_prepared_statements=False)
    await cur.execute("INSERT INTO tbl VALUES (:a, :b); COMMIT;", {'a': 2, 'b': 'bb'}, use_prepared_statements=False)

# Disable using server-side prepared statements at connection level
conn_info['use_prepared_statements'] = False
async with aiovertica.connect(**conn_info) as connection:
    cur = connection.cursor()

    # Try using prepared statements
    await cur.execute("INSERT INTO tbl VALUES (?, ?)", [3, 'foo'])
    # TypeError: not all arguments converted during string formatting

    await cur.execute("INSERT INTO tbl VALUES (?, ?)", [3, 'foo'], use_prepared_statements=True)

    # Query using named parameters
    await cur.execute("SELECT * FROM tbl WHERE a>=:n1 AND a<=:n2 ORDER BY a", {'n1': 2, 'n2': 4})
    await cur.fetchall()
    # [[2, 'bb'], [3, 'foo']]
```
Note: In other drivers, the batch insert is converted into a COPY statement by using prepared statements. vertica-python currently does not support that.

**Insert and commits**:

```python
cur = connection.cursor()

# inline commit (when 'use_prepared_statements' is False)
await cur.execute("INSERT INTO a_table (a, b) VALUES (1, 'aa'); commit;")

# commit in execution
await cur.execute("INSERT INTO a_table (a, b) VALUES (1, 'aa')")
await cur.execute("INSERT INTO a_table (a, b) VALUES (2, 'bb')")
await cur.execute("commit;")

# connection.commit()
await cur.execute("INSERT INTO a_table (a, b) VALUES (1, 'aa')")
await connection.commit()

# connection.rollback()
await cur.execute("INSERT INTO a_table (a, b) VALUES (0, 'bad')")
await connection.rollback()
```

**Autocommit**:

Session parameter AUTOCOMMIT can be configured by the connection option and the `Connection.autocommit` read/write attribute:
```python
import aiovertica

# Enable autocommit at startup
conn_info = {
    'host': '127.0.0.1',
    'user': 'some_user',
    'password': 'some_password',
    'database': 'a_database',
    # autocommit is off by default
    'autocommit': True,
}

async with aiovertica.connect(**conn_info) as connection:
    # Check current session autocommit setting
    print(connection.autocommit)    # should be True
    # If autocommit is True, statements automatically commit their transactions when they complete.
    
    # Set autocommit setting with attribute
    connection.autocommit = False
    print(connection.autocommit)    # should be False
    # If autocommit is False, the methods commit() or rollback() must be manually invoked to terminate the transaction.
```

> #### WARNING: set attr `autocommit` with instance may not work correctly in the current version

To set AUTOCOMMIT to a new value, vertica-python uses `Cursor.execute()` to execute a command internally, and that would clear your previous query results, so be sure to call `Cursor.fetch*()` to save your results before you set autocommit.


**Copy**:

There are 2 methods to do copy:

Method 1: "COPY FROM STDIN" sql with Cursor.copy()
```python
cur = connection.cursor()
await cur.copy("COPY test_copy (id, name) from stdin DELIMITER ',' ",  csv)
```

Where `csv` is either a string or a file-like object (specifically, any object with a `read()` method). If using a file, the data is streamed (in chunks of `buffer_size` bytes, which defaults to 128 * 2 ** 10).

```python
with open("/tmp/binary_file.csv", "rb") as fs:
    await cursor.copy(
        "COPY table(field1, field2) FROM STDIN DELIMITER ',' ENCLOSED BY '\"'",
        fs,
        buffer_size=65536
    )
```

Method 2: "COPY FROM LOCAL" sql with Cursor.execute() 

```python
import sys
import aiovertica

conn_info = {'host': '127.0.0.1',
             'user': 'some_user',
             'password': 'some_password',
             'database': 'a_database',
             # False by default
             #'disable_copy_local': True,
             # Don't support executing COPY LOCAL operations with prepared statements
             'use_prepared_statements': False
             }

async with aiovertica.connect(**conn_info) as connection:
    cur = connection.cursor()
    
    # Copy from local file
    await cur.execute(
        "COPY table(field1, field2) FROM LOCAL"
        " 'data_Jan_*.csv','data_Feb_01.csv' DELIMITER ','"
        " REJECTED DATA 'path/to/write/rejects.txt'"
        " EXCEPTIONS 'path/to/write/exceptions.txt'",
        buffer_size=65536
    )
    result = await cur.fetchall()
    print("Rows loaded:", result)
    
    # Copy from local stdin
    await cur.execute(
        "COPY table(field1, field2) FROM LOCAL STDIN DELIMITER ','",
        copy_stdin=sys.stdin
    )
    result = await cur.fetchall()
    print("Rows loaded:", result)

    # Copy from local stdin (compound statements)
    with open('f1.csv', 'r') as fs1, open('f2.csv', 'r') as fs2:
        await cur.execute(
            "COPY tlb1(field1, field2) FROM LOCAL STDIN DELIMITER ',';"
            "COPY tlb2(field1, field2) FROM LOCAL STDIN DELIMITER ',';",
            copy_stdin=[fs1, fs2], buffer_size=65536
        )
    result = await cur.fetchall()
    print("Rows loaded 1:", result)
    await cur.nextset()
    result = await cur.fetchall()
    print("Rows loaded 2:", result)
```
When connection option `disable_copy_local` set to True, disables COPY LOCAL operations, including copying data from local files/stdin and using local files to store data and exceptions. You can use this property to prevent users from writing to and copying from files on a Vertica host, including an MC host. Note that this property doesn't apply to `Cursor.copy()`.

The data for copying from/writing to local files is streamed in chunks of `buffer_size` bytes, which defaults to 128 * 2 ** 10.

When executing "COPY FROM LOCAL STDIN", `copy_stdin` should be a file-like object or a list of file-like objects (specifically, any object with a `read()` method).

**Cancel the current database operation**:

`Connection.cancel()` interrupts the processing of the current operation. Interrupting query execution will cause the cancelled method to raise a `vertica_python.errors.QueryCanceled`. If no query is being executed, it does nothing. You can call this function from a different thread/process than the one currently executing a database operation.

```python
from multiprocessing import Process
import asyncio
import aiovertica

async def cancel_query(connection, timeout=5):
    await asyncio.sleep(timeout)
    await connection.cancel()

# Example 1: Cancel the query before Cursor.execute() return.
#            The query stops executing in a shorter time after the cancel message is sent.
async with aiovertica.connect(**conn_info) as conn:
    cur = conn.cursor()

    # Call cancel() from a different process
    p1 = Process(target=cancel_query, args=(conn,))
    p1.start()

    try:
        cur.execute("<Long running query>")
    except aiovertica.errors.QueryCanceled as e:
        pass

    p1.join()
    
# Example 2: Cancel the query after Cursor.execute() return.
#            Less number of rows read after the cancel message is sent.
with aiovertica.connect(**conn_info) as conn:
    cur = conn.cursor()
    await cur.execute("SELECT id, time FROM large_table")
    nCount = 0
    try:
        result = await cur.fetchone()
        while result:
            nCount += 1
            if nCount == 100:
                conn.cancel()
            result = await cur.fetchone()
    except aiovertica.errors.QueryCanceled as e:
        pass
        # nCount is less than the number of rows in large_table

```



## Rowcount oddities

vertica_python behaves a bit differently than dbapi when returning rowcounts.

After a select execution, the rowcount will be -1, indicating that the row count is unknown. The rowcount value will be updated as data is streamed.

```python
await cur.execute('SELECT 10 things')

cur.rowcount == -1  # indicates unknown rowcount

await cur.fetchone()
cur.rowcount == 1
await cur.fetchone()
cur.rowcount == 2
await cur.fetchall()
cur.rowcount == 10
```

After an insert/update/delete, the rowcount will be returned as a single element row:

```python
await cur.execute("DELETE 3 things")

cur.rowcount == -1  # indicates unknown rowcount
result = await cur.fetchone()
result[0] == 3
```

## Nextset

If you execute multiple statements in a single call to execute(), you can use cursor.nextset() to retrieve all of the data.

```python
await cur.execute('SELECT 1; SELECT 2;')

await cur.fetchone()
# [1]
await cur.fetchone()
# None

await cur.nextset()
# True

await cur.fetchone()
# [2]
await cur.fetchone()
# None

await cur.nextset()
# None
```

## UTF-8 encoding issues

While Vertica expects varchars stored to be UTF-8 encoded, sometimes invalid strings get into the database. You can specify how to handle reading these characters using the unicode_error connection option. This uses the same values as the unicode type (https://docs.python.org/2/library/functions.html#unicode)

```python
import aiovertica

conn = await aiovertica.connect({..., 'unicode_error': 'strict'})
cur = conn.cursor()
await cur.execute(r"SELECT E'\xC2'")
await cur.fetchone()
# caught 'utf8' codec can't decode byte 0xc2 in position 0: unexpected end of data

conn = await aiovertica.connect({..., 'unicode_error': 'strict'})
cur = conn.cursor()
await cur.execute(r"SELECT E'\xC2'")
await cur.fetchone()
# �

conn = await aiovertica.connect({..., 'unicode_error': 'strict'})
cur = conn.cursor()
await cur.execute(r"SELECT E'\xC2'")
await cur.fetchone()
# 
```

## License

Apache 2.0 License, please see `LICENSE` for details.

## Contributing guidelines

Have a bug or an idea? Please see `CONTRIBUTING.md` for details.

## Acknowledgements

I would like to express my deep gratitude to the team 
that worked on the [vertica_python](https://github.com/vertica/vertica-python/) for writing at least a synchronous driver - this greatly 
simplified the work. I have tried to keep all the licenses and the list of authors from 
the original project.