# test/requires.py
# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

"""Global database feature support policy.

Provides decorators to mark tests requiring specific feature support from the
target database.

"""

from testing import \
     _block_unconditionally as no_support, \
     _chain_decorators_on, \
     exclude, \
     emits_warning_on,\
     skip_if,\
     fails_on,\
     fails_on_everything_except

import testing
import sys

def deferrable_constraints(fn):
    """Target database must support derferable constraints."""
    return _chain_decorators_on(
        fn,
        no_support('firebird', 'not supported by database'),
        no_support('mysql', 'not supported by database'),
        no_support('mssql', 'not supported by database'),
        )

def foreign_keys(fn):
    """Target database must support foreign keys."""
    return _chain_decorators_on(
        fn,
        no_support('sqlite', 'not supported by database'),
        )


def unbounded_varchar(fn):
    """Target database must support VARCHAR with no length"""
    return _chain_decorators_on(
        fn,
        no_support('firebird', 'not supported by database'),
        no_support('oracle', 'not supported by database'),
        no_support('mysql', 'not supported by database'),
    )

def boolean_col_expressions(fn):
    """Target database must support boolean expressions as columns"""
    return _chain_decorators_on(
        fn,
        no_support('firebird', 'not supported by database'),
        no_support('oracle', 'not supported by database'),
        no_support('mssql', 'not supported by database'),
        no_support('sybase', 'not supported by database'),
        no_support('maxdb', 'FIXME: verify not supported by database'),
        no_support('informix', 'not supported by database'),
    )

def identity(fn):
    """Target database must support GENERATED AS IDENTITY or a facsimile.

    Includes GENERATED AS IDENTITY, AUTOINCREMENT, AUTO_INCREMENT, or other
    column DDL feature that fills in a DB-generated identifier at INSERT-time
    without requiring pre-execution of a SEQUENCE or other artifact.

    """
    return _chain_decorators_on(
        fn,
        no_support('firebird', 'not supported by database'),
        no_support('oracle', 'not supported by database'),
        no_support('postgresql', 'not supported by database'),
        no_support('sybase', 'not supported by database'),
        )

def independent_cursors(fn):
    """Target must support simultaneous, independent database cursors on a single connection."""

    return _chain_decorators_on(
        fn,
        no_support('mssql+pyodbc', 'no driver support'),
        no_support('mssql+mxodbc', 'no driver support'),
        )

def independent_connections(fn):
    """Target must support simultaneous, independent database connections."""

    # This is also true of some configurations of UnixODBC and probably win32
    # ODBC as well.
    return _chain_decorators_on(
        fn,
        no_support('sqlite', 'no driver support'),
        exclude('mssql', '<', (9, 0, 0),
                'SQL Server 2005+ is required for independent connections'),
        )

def row_triggers(fn):
    """Target must support standard statement-running EACH ROW triggers."""
    return _chain_decorators_on(
        fn,
        # no access to same table
        no_support('mysql', 'requires SUPER priv'),
        exclude('mysql', '<', (5, 0, 10), 'not supported by database'),

        # huh?  TODO: implement triggers for PG tests, remove this
        no_support('postgresql', 'PG triggers need to be implemented for tests'),
        )

def correlated_outer_joins(fn):
    """Target must support an outer join to a subquery which correlates to the parent."""

    return _chain_decorators_on(
        fn,
        no_support('oracle', 'Raises "ORA-01799: a column may not be outer-joined to a subquery"')
    )

def savepoints(fn):
    """Target database must support savepoints."""
    return _chain_decorators_on(
        fn,
        emits_warning_on('mssql', 'Savepoint support in mssql is experimental and may lead to data loss.'),
        no_support('access', 'not supported by database'),
        no_support('sqlite', 'not supported by database'),
        no_support('sybase', 'FIXME: guessing, needs confirmation'),
        exclude('mysql', '<', (5, 0, 3), 'not supported by database'),
        exclude('informix', '<', (11, 55, 'xC3'), 'not supported by database'),
        )

def denormalized_names(fn):
    """Target database must have 'denormalized', i.e. UPPERCASE as case insensitive names."""

    return skip_if(
                lambda: not testing.db.dialect.requires_name_normalize,
                "Backend does not require denomralized names."
            )(fn)

def schemas(fn):
    """Target database must support external schemas, and have one named 'test_schema'."""

    return _chain_decorators_on(
        fn,
        no_support('sqlite', 'no schema support'),
        no_support('firebird', 'no schema support')
    )

def sequences(fn):
    """Target database must support SEQUENCEs."""
    return _chain_decorators_on(
        fn,
        no_support('access', 'no SEQUENCE support'),
        no_support('mssql', 'no SEQUENCE support'),
        no_support('mysql', 'no SEQUENCE support'),
        no_support('sqlite', 'no SEQUENCE support'),
        no_support('sybase', 'no SEQUENCE support'),
        no_support('informix', 'no SEQUENCE support'),
        )

def update_nowait(fn):
    """Target database must support SELECT...FOR UPDATE NOWAIT"""
    return _chain_decorators_on(
        fn,
        no_support('access', 'no FOR UPDATE NOWAIT support'),
        no_support('firebird', 'no FOR UPDATE NOWAIT support'),
        no_support('mssql', 'no FOR UPDATE NOWAIT support'),
        no_support('mysql', 'no FOR UPDATE NOWAIT support'),
        no_support('sqlite', 'no FOR UPDATE NOWAIT support'),
        no_support('sybase', 'no FOR UPDATE NOWAIT support'),
    )

def subqueries(fn):
    """Target database must support subqueries."""
    return _chain_decorators_on(
        fn,
        exclude('mysql', '<', (4, 1, 1), 'no subquery support'),
        )

def intersect(fn):
    """Target database must support INTERSECT or equivlaent."""
    return _chain_decorators_on(
        fn,
        fails_on('firebird', 'no support for INTERSECT'),
        fails_on('mysql', 'no support for INTERSECT'),
        fails_on('sybase', 'no support for INTERSECT'),
        fails_on('informix', 'no support for INTERSECT'),
    )

def except_(fn):
    """Target database must support EXCEPT or equivlaent (i.e. MINUS)."""
    return _chain_decorators_on(
        fn,
        fails_on('firebird', 'no support for EXCEPT'),
        fails_on('mysql', 'no support for EXCEPT'),
        fails_on('sybase', 'no support for EXCEPT'),
        fails_on('informix', 'no support for EXCEPT'),
    )

def offset(fn):
    """Target database must support some method of adding OFFSET or equivalent to a result set."""
    return _chain_decorators_on(
        fn,
        fails_on('sybase', 'no support for OFFSET or equivalent'),
    )

def returning(fn):
    return _chain_decorators_on(
        fn,
        no_support('access', 'not supported by database'),
        no_support('sqlite', 'not supported by database'),
        no_support('mysql', 'not supported by database'),
        no_support('maxdb', 'not supported by database'),
        no_support('sybase', 'not supported by database'),
        no_support('informix', 'not supported by database'),
    )

def two_phase_transactions(fn):
    """Target database must support two-phase transactions."""
    return _chain_decorators_on(
        fn,
        no_support('access', 'not supported by database'),
        no_support('firebird', 'no SA implementation'),
        no_support('maxdb', 'not supported by database'),
        no_support('mssql', 'FIXME: guessing, needs confirmation'),
        no_support('oracle', 'no SA implementation'),
        no_support('sqlite', 'not supported by database'),
        no_support('sybase', 'FIXME: guessing, needs confirmation'),
        no_support('postgresql+zxjdbc', 'FIXME: JDBC driver confuses the transaction state, may '
                   'need separate XA implementation'),
        exclude('mysql', '<', (5, 0, 3), 'not supported by database'),
        )

def unicode_connections(fn):
    """Target driver must support some encoding of Unicode across the wire."""
    # TODO: expand to exclude MySQLdb versions w/ broken unicode
    return _chain_decorators_on(
        fn,
        exclude('mysql', '<', (4, 1, 1), 'no unicode connection support'),
        )

def unicode_ddl(fn):
    """Target driver must support some encoding of Unicode across the wire."""
    # TODO: expand to exclude MySQLdb versions w/ broken unicode
    return _chain_decorators_on(
        fn,
        no_support('maxdb', 'database support flakey'),
        no_support('oracle', 'FIXME: no support in database?'),
        no_support('sybase', 'FIXME: guessing, needs confirmation'),
        no_support('mssql+pymssql', 'no FreeTDS support'),
        exclude('mysql', '<', (4, 1, 1), 'no unicode connection support'),
        )

def sane_rowcount(fn):
    return _chain_decorators_on(
        fn,
        skip_if(lambda: not testing.db.dialect.supports_sane_rowcount)
    )

def cextensions(fn):
    return _chain_decorators_on(
        fn,
        skip_if(lambda: not _has_cextensions(), "C extensions not installed")
    )

def dbapi_lastrowid(fn):
    return _chain_decorators_on(
        fn,
        fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', 'sqlite+pysqlite')
    )

def sane_multi_rowcount(fn):
    return _chain_decorators_on(
        fn,
        skip_if(lambda: not testing.db.dialect.supports_sane_multi_rowcount)
    )

def reflects_pk_names(fn):
    """Target driver reflects the name of primary key constraints."""
    return _chain_decorators_on(
        fn,
        fails_on_everything_except('postgresql', 'oracle')
    )

def python2(fn):
    return _chain_decorators_on(
        fn,
        skip_if(
            lambda: sys.version_info >= (3,),
            "Python version 2.xx is required."
            )
    )

def python26(fn):
    return _chain_decorators_on(
        fn,
        skip_if(
            lambda: sys.version_info < (2, 6),
            "Python version 2.6 or greater is required"
        )
    )

def python25(fn):
    return _chain_decorators_on(
        fn,
        skip_if(
            lambda: sys.version_info < (2, 5),
            "Python version 2.5 or greater is required"
        )
    )

def _has_cextensions():
    try:
        from sqlalchemy import cresultproxy, cprocessors
        return True
    except ImportError:
        return False

def _has_sqlite():
    from sqlalchemy import create_engine
    try:
        e = create_engine('sqlite://')
        return True
    except ImportError:
        return False

def _has_mysql_on_windows():
    return testing.against('mysql') and \
            testing.db.dialect._server_casing == 1

def sqlite(fn):
    return _chain_decorators_on(
        fn,
        skip_if(lambda: not _has_sqlite())
    )

def skip_mysql_on_windows(fn):
    """Catchall for a large variety of MySQL on Windows failures"""

    return _chain_decorators_on(
        fn,
        skip_if(_has_mysql_on_windows,
            "Not supported on MySQL + Windows"
        )
    )

def english_locale_on_postgresql(fn):
    return _chain_decorators_on(
        fn,
        skip_if(lambda: testing.against('postgresql') \
                and not testing.db.scalar('SHOW LC_COLLATE').startswith('en'))
    )
