You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1519 lines
42 KiB
Python
1519 lines
42 KiB
Python
# testing/requirements.py
|
|
# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
|
|
# <see AUTHORS file>
|
|
#
|
|
# This module is part of SQLAlchemy and is released under
|
|
# the MIT License: https://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.
|
|
|
|
External dialect test suites should subclass SuiteRequirements
|
|
to provide specific inclusion/exclusions.
|
|
|
|
"""
|
|
|
|
import platform
|
|
import sys
|
|
|
|
from . import exclusions
|
|
from . import only_on
|
|
from .. import util
|
|
from ..pool import QueuePool
|
|
|
|
|
|
class Requirements(object):
|
|
pass
|
|
|
|
|
|
class SuiteRequirements(Requirements):
|
|
@property
|
|
def create_table(self):
|
|
"""target platform can emit basic CreateTable DDL."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def drop_table(self):
|
|
"""target platform can emit basic DropTable DDL."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def table_ddl_if_exists(self):
|
|
"""target platform supports IF NOT EXISTS / IF EXISTS for tables."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def index_ddl_if_exists(self):
|
|
"""target platform supports IF NOT EXISTS / IF EXISTS for indexes."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def foreign_keys(self):
|
|
"""Target database must support foreign keys."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def table_value_constructor(self):
|
|
"""Database / dialect supports a query like::
|
|
|
|
SELECT * FROM VALUES ( (c1, c2), (c1, c2), ...)
|
|
AS some_table(col1, col2)
|
|
|
|
SQLAlchemy generates this with the :func:`_sql.values` function.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def standard_cursor_sql(self):
|
|
"""Target database passes SQL-92 style statements to cursor.execute()
|
|
when a statement like select() or insert() is run.
|
|
|
|
A very small portion of dialect-level tests will ensure that certain
|
|
conditions are present in SQL strings, and these tests use very basic
|
|
SQL that will work on any SQL-like platform in order to assert results.
|
|
|
|
It's normally a given for any pep-249 DBAPI that a statement like
|
|
"SELECT id, name FROM table WHERE some_table.id=5" will work.
|
|
However, there are dialects that don't actually produce SQL Strings
|
|
and instead may work with symbolic objects instead, or dialects that
|
|
aren't working with SQL, so for those this requirement can be marked
|
|
as excluded.
|
|
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def on_update_cascade(self):
|
|
"""target database must support ON UPDATE..CASCADE behavior in
|
|
foreign keys."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def non_updating_cascade(self):
|
|
"""target database must *not* support ON UPDATE..CASCADE behavior in
|
|
foreign keys."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def deferrable_fks(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def on_update_or_deferrable_fks(self):
|
|
# TODO: exclusions should be composable,
|
|
# somehow only_if([x, y]) isn't working here, negation/conjunctions
|
|
# getting confused.
|
|
return exclusions.only_if(
|
|
lambda: self.on_update_cascade.enabled
|
|
or self.deferrable_fks.enabled
|
|
)
|
|
|
|
@property
|
|
def queue_pool(self):
|
|
"""target database is using QueuePool"""
|
|
|
|
def go(config):
|
|
return isinstance(config.db.pool, QueuePool)
|
|
|
|
return exclusions.only_if(go)
|
|
|
|
@property
|
|
def self_referential_foreign_keys(self):
|
|
"""Target database must support self-referential foreign keys."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def foreign_key_ddl(self):
|
|
"""Target database must support the DDL phrases for FOREIGN KEY."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def named_constraints(self):
|
|
"""target database must support names for constraints."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def implicitly_named_constraints(self):
|
|
"""target database must apply names to unnamed constraints."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def subqueries(self):
|
|
"""Target database must support subqueries."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def offset(self):
|
|
"""target database can render OFFSET, or an equivalent, in a
|
|
SELECT.
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def bound_limit_offset(self):
|
|
"""target database can render LIMIT and/or OFFSET using a bound
|
|
parameter
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def sql_expression_limit_offset(self):
|
|
"""target database can render LIMIT and/or OFFSET with a complete
|
|
SQL expression, such as one that uses the addition operator.
|
|
parameter
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def parens_in_union_contained_select_w_limit_offset(self):
|
|
"""Target database must support parenthesized SELECT in UNION
|
|
when LIMIT/OFFSET is specifically present.
|
|
|
|
E.g. (SELECT ...) UNION (SELECT ..)
|
|
|
|
This is known to fail on SQLite.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def parens_in_union_contained_select_wo_limit_offset(self):
|
|
"""Target database must support parenthesized SELECT in UNION
|
|
when OFFSET/LIMIT is specifically not present.
|
|
|
|
E.g. (SELECT ... LIMIT ..) UNION (SELECT .. OFFSET ..)
|
|
|
|
This is known to fail on SQLite. It also fails on Oracle
|
|
because without LIMIT/OFFSET, there is currently no step that
|
|
creates an additional subquery.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def boolean_col_expressions(self):
|
|
"""Target database must support boolean expressions as columns"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def nullable_booleans(self):
|
|
"""Target database allows boolean columns to store NULL."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def nullsordering(self):
|
|
"""Target backends that support nulls ordering."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def standalone_binds(self):
|
|
"""target database/driver supports bound parameters as column
|
|
expressions without being in the context of a typed column.
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def standalone_null_binds_whereclause(self):
|
|
"""target database/driver supports bound parameters with NULL in the
|
|
WHERE clause, in situations where it has to be typed.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def intersect(self):
|
|
"""Target database must support INTERSECT or equivalent."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def except_(self):
|
|
"""Target database must support EXCEPT or equivalent (i.e. MINUS)."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def window_functions(self):
|
|
"""Target database must support window functions."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def ctes(self):
|
|
"""Target database supports CTEs"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def ctes_with_update_delete(self):
|
|
"""target database supports CTES that ride on top of a normal UPDATE
|
|
or DELETE statement which refers to the CTE in a correlated subquery.
|
|
|
|
"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def ctes_on_dml(self):
|
|
"""target database supports CTES which consist of INSERT, UPDATE
|
|
or DELETE *within* the CTE, e.g. WITH x AS (UPDATE....)"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def autoincrement_insert(self):
|
|
"""target platform generates new surrogate integer primary key values
|
|
when insert() is executed, excluding the pk column."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def fetch_rows_post_commit(self):
|
|
"""target platform will allow cursor.fetchone() to proceed after a
|
|
COMMIT.
|
|
|
|
Typically this refers to an INSERT statement with RETURNING which
|
|
is invoked within "autocommit". If the row can be returned
|
|
after the autocommit, then this rule can be open.
|
|
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def group_by_complex_expression(self):
|
|
"""target platform supports SQL expressions in GROUP BY
|
|
|
|
e.g.
|
|
|
|
SELECT x + y AS somelabel FROM table GROUP BY x + y
|
|
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def sane_rowcount(self):
|
|
return exclusions.skip_if(
|
|
lambda config: not config.db.dialect.supports_sane_rowcount,
|
|
"driver doesn't support 'sane' rowcount",
|
|
)
|
|
|
|
@property
|
|
def sane_multi_rowcount(self):
|
|
return exclusions.fails_if(
|
|
lambda config: not config.db.dialect.supports_sane_multi_rowcount,
|
|
"driver %(driver)s %(doesnt_support)s 'sane' multi row count",
|
|
)
|
|
|
|
@property
|
|
def sane_rowcount_w_returning(self):
|
|
return exclusions.fails_if(
|
|
lambda config: not (
|
|
config.db.dialect.supports_sane_rowcount_returning
|
|
),
|
|
"driver doesn't support 'sane' rowcount when returning is on",
|
|
)
|
|
|
|
@property
|
|
def empty_inserts(self):
|
|
"""target platform supports INSERT with no values, i.e.
|
|
INSERT DEFAULT VALUES or equivalent."""
|
|
|
|
return exclusions.only_if(
|
|
lambda config: config.db.dialect.supports_empty_insert
|
|
or config.db.dialect.supports_default_values
|
|
or config.db.dialect.supports_default_metavalue,
|
|
"empty inserts not supported",
|
|
)
|
|
|
|
@property
|
|
def empty_inserts_executemany(self):
|
|
"""target platform supports INSERT with no values, i.e.
|
|
INSERT DEFAULT VALUES or equivalent, within executemany()"""
|
|
|
|
return self.empty_inserts
|
|
|
|
@property
|
|
def insert_from_select(self):
|
|
"""target platform supports INSERT from a SELECT."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def full_returning(self):
|
|
"""target platform supports RETURNING completely, including
|
|
multiple rows returned.
|
|
|
|
"""
|
|
|
|
return exclusions.only_if(
|
|
lambda config: config.db.dialect.full_returning,
|
|
"%(database)s %(does_support)s 'RETURNING of multiple rows'",
|
|
)
|
|
|
|
@property
|
|
def insert_executemany_returning(self):
|
|
"""target platform supports RETURNING when INSERT is used with
|
|
executemany(), e.g. multiple parameter sets, indicating
|
|
as many rows come back as do parameter sets were passed.
|
|
|
|
"""
|
|
|
|
return exclusions.only_if(
|
|
lambda config: config.db.dialect.insert_executemany_returning,
|
|
"%(database)s %(does_support)s 'RETURNING of "
|
|
"multiple rows with INSERT executemany'",
|
|
)
|
|
|
|
@property
|
|
def returning(self):
|
|
"""target platform supports RETURNING for at least one row.
|
|
|
|
.. seealso::
|
|
|
|
:attr:`.Requirements.full_returning`
|
|
|
|
"""
|
|
|
|
return exclusions.only_if(
|
|
lambda config: config.db.dialect.implicit_returning,
|
|
"%(database)s %(does_support)s 'RETURNING of a single row'",
|
|
)
|
|
|
|
@property
|
|
def tuple_in(self):
|
|
"""Target platform supports the syntax
|
|
"(x, y) IN ((x1, y1), (x2, y2), ...)"
|
|
"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def tuple_in_w_empty(self):
|
|
"""Target platform tuple IN w/ empty set"""
|
|
return self.tuple_in
|
|
|
|
@property
|
|
def duplicate_names_in_cursor_description(self):
|
|
"""target platform supports a SELECT statement that has
|
|
the same name repeated more than once in the columns list."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def denormalized_names(self):
|
|
"""Target database must have 'denormalized', i.e.
|
|
UPPERCASE as case insensitive names."""
|
|
|
|
return exclusions.skip_if(
|
|
lambda config: not config.db.dialect.requires_name_normalize,
|
|
"Backend does not require denormalized names.",
|
|
)
|
|
|
|
@property
|
|
def multivalues_inserts(self):
|
|
"""target database must support multiple VALUES clauses in an
|
|
INSERT statement."""
|
|
|
|
return exclusions.skip_if(
|
|
lambda config: not config.db.dialect.supports_multivalues_insert,
|
|
"Backend does not support multirow inserts.",
|
|
)
|
|
|
|
@property
|
|
def implements_get_lastrowid(self):
|
|
"""target dialect implements the executioncontext.get_lastrowid()
|
|
method without reliance on RETURNING.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def emulated_lastrowid(self):
|
|
"""target dialect retrieves cursor.lastrowid, or fetches
|
|
from a database-side function after an insert() construct executes,
|
|
within the get_lastrowid() method.
|
|
|
|
Only dialects that "pre-execute", or need RETURNING to get last
|
|
inserted id, would return closed/fail/skip for this.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def emulated_lastrowid_even_with_sequences(self):
|
|
"""target dialect retrieves cursor.lastrowid or an equivalent
|
|
after an insert() construct executes, even if the table has a
|
|
Sequence on it.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def dbapi_lastrowid(self):
|
|
"""target platform includes a 'lastrowid' accessor on the DBAPI
|
|
cursor object.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def views(self):
|
|
"""Target database must support VIEWs."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def schemas(self):
|
|
"""Target database must support external schemas, and have one
|
|
named 'test_schema'."""
|
|
|
|
return only_on(lambda config: config.db.dialect.supports_schemas)
|
|
|
|
@property
|
|
def cross_schema_fk_reflection(self):
|
|
"""target system must support reflection of inter-schema
|
|
foreign keys"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def foreign_key_constraint_name_reflection(self):
|
|
"""Target supports refleciton of FOREIGN KEY constraints and
|
|
will return the name of the constraint that was used in the
|
|
"CONSTRAINT <name> FOREIGN KEY" DDL.
|
|
|
|
MySQL prior to version 8 and MariaDB prior to version 10.5
|
|
don't support this.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def implicit_default_schema(self):
|
|
"""target system has a strong concept of 'default' schema that can
|
|
be referred to implicitly.
|
|
|
|
basically, PostgreSQL.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def default_schema_name_switch(self):
|
|
"""target dialect implements provisioning module including
|
|
set_default_schema_on_connection"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def server_side_cursors(self):
|
|
"""Target dialect must support server side cursors."""
|
|
|
|
return exclusions.only_if(
|
|
[lambda config: config.db.dialect.supports_server_side_cursors],
|
|
"no server side cursors support",
|
|
)
|
|
|
|
@property
|
|
def sequences(self):
|
|
"""Target database must support SEQUENCEs."""
|
|
|
|
return exclusions.only_if(
|
|
[lambda config: config.db.dialect.supports_sequences],
|
|
"no sequence support",
|
|
)
|
|
|
|
@property
|
|
def no_sequences(self):
|
|
"""the opposite of "sequences", DB does not support sequences at
|
|
all."""
|
|
|
|
return exclusions.NotPredicate(self.sequences)
|
|
|
|
@property
|
|
def sequences_optional(self):
|
|
"""Target database supports sequences, but also optionally
|
|
as a means of generating new PK values."""
|
|
|
|
return exclusions.only_if(
|
|
[
|
|
lambda config: config.db.dialect.supports_sequences
|
|
and config.db.dialect.sequences_optional
|
|
],
|
|
"no sequence support, or sequences not optional",
|
|
)
|
|
|
|
@property
|
|
def supports_lastrowid(self):
|
|
"""target database / driver supports cursor.lastrowid as a means
|
|
of retrieving the last inserted primary key value.
|
|
|
|
note that if the target DB supports sequences also, this is still
|
|
assumed to work. This is a new use case brought on by MariaDB 10.3.
|
|
|
|
"""
|
|
return exclusions.only_if(
|
|
[lambda config: config.db.dialect.postfetch_lastrowid]
|
|
)
|
|
|
|
@property
|
|
def no_lastrowid_support(self):
|
|
"""the opposite of supports_lastrowid"""
|
|
return exclusions.only_if(
|
|
[lambda config: not config.db.dialect.postfetch_lastrowid]
|
|
)
|
|
|
|
@property
|
|
def reflects_pk_names(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def table_reflection(self):
|
|
"""target database has general support for table reflection"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def reflect_tables_no_columns(self):
|
|
"""target database supports creation and reflection of tables with no
|
|
columns, or at least tables that seem to have no columns."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def comment_reflection(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def view_column_reflection(self):
|
|
"""target database must support retrieval of the columns in a view,
|
|
similarly to how a table is inspected.
|
|
|
|
This does not include the full CREATE VIEW definition.
|
|
|
|
"""
|
|
return self.views
|
|
|
|
@property
|
|
def view_reflection(self):
|
|
"""target database must support inspection of the full CREATE VIEW
|
|
definition."""
|
|
return self.views
|
|
|
|
@property
|
|
def schema_reflection(self):
|
|
return self.schemas
|
|
|
|
@property
|
|
def primary_key_constraint_reflection(self):
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def foreign_key_constraint_reflection(self):
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def foreign_key_constraint_option_reflection_ondelete(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fk_constraint_option_reflection_ondelete_restrict(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fk_constraint_option_reflection_ondelete_noaction(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def foreign_key_constraint_option_reflection_onupdate(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fk_constraint_option_reflection_onupdate_restrict(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def temp_table_reflection(self):
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def temp_table_reflect_indexes(self):
|
|
return self.temp_table_reflection
|
|
|
|
@property
|
|
def temp_table_names(self):
|
|
"""target dialect supports listing of temporary table names"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def temporary_tables(self):
|
|
"""target database supports temporary tables"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def temporary_views(self):
|
|
"""target database supports temporary views"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def index_reflection(self):
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def index_reflects_included_columns(self):
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def indexes_with_ascdesc(self):
|
|
"""target database supports CREATE INDEX with per-column ASC/DESC."""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def indexes_with_expressions(self):
|
|
"""target database supports CREATE INDEX against SQL expressions."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def unique_constraint_reflection(self):
|
|
"""target dialect supports reflection of unique constraints"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def check_constraint_reflection(self):
|
|
"""target dialect supports reflection of check constraints"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def duplicate_key_raises_integrity_error(self):
|
|
"""target dialect raises IntegrityError when reporting an INSERT
|
|
with a primary key violation. (hint: it should)
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def unbounded_varchar(self):
|
|
"""Target database must support VARCHAR with no length"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def unicode_data(self):
|
|
"""Target database/dialect must support Python unicode objects with
|
|
non-ASCII characters represented, delivered as bound parameters
|
|
as well as in result rows.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def unicode_ddl(self):
|
|
"""Target driver must support some degree of non-ascii symbol
|
|
names.
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def symbol_names_w_double_quote(self):
|
|
"""Target driver can create tables with a name like 'some " table'"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def datetime_literals(self):
|
|
"""target dialect supports rendering of a date, time, or datetime as a
|
|
literal string, e.g. via the TypeEngine.literal_processor() method.
|
|
|
|
"""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def datetime(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.datetime() objects."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def datetime_timezone(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.datetime() with tzinfo with DateTime(timezone=True)."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def time_timezone(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.time() with tzinfo with Time(timezone=True)."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def datetime_implicit_bound(self):
|
|
"""target dialect when given a datetime object will bind it such
|
|
that the database server knows the object is a datetime, and not
|
|
a plain string.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def datetime_microseconds(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.datetime() with microsecond objects."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def timestamp_microseconds(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.datetime() with microsecond objects but only
|
|
if TIMESTAMP is used."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def timestamp_microseconds_implicit_bound(self):
|
|
"""target dialect when given a datetime object which also includes
|
|
a microseconds portion when using the TIMESTAMP data type
|
|
will bind it such that the database server knows
|
|
the object is a datetime with microseconds, and not a plain string.
|
|
|
|
"""
|
|
return self.timestamp_microseconds
|
|
|
|
@property
|
|
def datetime_historic(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.datetime() objects with historic (pre 1970) values."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def date(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.date() objects."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def date_coerces_from_datetime(self):
|
|
"""target dialect accepts a datetime object as the target
|
|
of a date column."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def date_historic(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.datetime() objects with historic (pre 1970) values."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def time(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.time() objects."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def time_microseconds(self):
|
|
"""target dialect supports representation of Python
|
|
datetime.time() with microsecond objects."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def binary_comparisons(self):
|
|
"""target database/driver can allow BLOB/BINARY fields to be compared
|
|
against a bound parameter value.
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def binary_literals(self):
|
|
"""target backend supports simple binary literals, e.g. an
|
|
expression like::
|
|
|
|
SELECT CAST('foo' AS BINARY)
|
|
|
|
Where ``BINARY`` is the type emitted from :class:`.LargeBinary`,
|
|
e.g. it could be ``BLOB`` or similar.
|
|
|
|
Basically fails on Oracle.
|
|
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def autocommit(self):
|
|
"""target dialect supports 'AUTOCOMMIT' as an isolation_level"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def isolation_level(self):
|
|
"""target dialect supports general isolation level settings.
|
|
|
|
Note that this requirement, when enabled, also requires that
|
|
the get_isolation_levels() method be implemented.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
def get_isolation_levels(self, config):
|
|
"""Return a structure of supported isolation levels for the current
|
|
testing dialect.
|
|
|
|
The structure indicates to the testing suite what the expected
|
|
"default" isolation should be, as well as the other values that
|
|
are accepted. The dictionary has two keys, "default" and "supported".
|
|
The "supported" key refers to a list of all supported levels and
|
|
it should include AUTOCOMMIT if the dialect supports it.
|
|
|
|
If the :meth:`.DefaultRequirements.isolation_level` requirement is
|
|
not open, then this method has no return value.
|
|
|
|
E.g.::
|
|
|
|
>>> testing.requirements.get_isolation_levels()
|
|
{
|
|
"default": "READ_COMMITTED",
|
|
"supported": [
|
|
"SERIALIZABLE", "READ UNCOMMITTED",
|
|
"READ COMMITTED", "REPEATABLE READ",
|
|
"AUTOCOMMIT"
|
|
]
|
|
}
|
|
"""
|
|
|
|
@property
|
|
def json_type(self):
|
|
"""target platform implements a native JSON type."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def json_array_indexes(self):
|
|
"""target platform supports numeric array indexes
|
|
within a JSON structure"""
|
|
|
|
return self.json_type
|
|
|
|
@property
|
|
def json_index_supplementary_unicode_element(self):
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def legacy_unconditional_json_extract(self):
|
|
"""Backend has a JSON_EXTRACT or similar function that returns a
|
|
valid JSON string in all cases.
|
|
|
|
Used to test a legacy feature and is not needed.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def precision_numerics_general(self):
|
|
"""target backend has general support for moderately high-precision
|
|
numerics."""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def precision_numerics_enotation_small(self):
|
|
"""target backend supports Decimal() objects using E notation
|
|
to represent very small values."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def precision_numerics_enotation_large(self):
|
|
"""target backend supports Decimal() objects using E notation
|
|
to represent very large values."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def precision_numerics_many_significant_digits(self):
|
|
"""target backend supports values with many digits on both sides,
|
|
such as 319438950232418390.273596, 87673.594069654243
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def cast_precision_numerics_many_significant_digits(self):
|
|
"""same as precision_numerics_many_significant_digits but within the
|
|
context of a CAST statement (hello MySQL)
|
|
|
|
"""
|
|
return self.precision_numerics_many_significant_digits
|
|
|
|
@property
|
|
def implicit_decimal_binds(self):
|
|
"""target backend will return a selected Decimal as a Decimal, not
|
|
a string.
|
|
|
|
e.g.::
|
|
|
|
expr = decimal.Decimal("15.7563")
|
|
|
|
value = e.scalar(
|
|
select(literal(expr))
|
|
)
|
|
|
|
assert value == expr
|
|
|
|
See :ticket:`4036`
|
|
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def nested_aggregates(self):
|
|
"""target database can select an aggregate from a subquery that's
|
|
also using an aggregate
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def recursive_fk_cascade(self):
|
|
"""target database must support ON DELETE CASCADE on a self-referential
|
|
foreign key
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def precision_numerics_retains_significant_digits(self):
|
|
"""A precision numeric type will return empty significant digits,
|
|
i.e. a value such as 10.000 will come back in Decimal form with
|
|
the .000 maintained."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def infinity_floats(self):
|
|
"""The Float type can persist and load float('inf'), float('-inf')."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def precision_generic_float_type(self):
|
|
"""target backend will return native floating point numbers with at
|
|
least seven decimal places when using the generic Float type.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def floats_to_four_decimals(self):
|
|
"""target backend can return a floating-point number with four
|
|
significant digits (such as 15.7563) accurately
|
|
(i.e. without FP inaccuracies, such as 15.75629997253418).
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def fetch_null_from_numeric(self):
|
|
"""target backend doesn't crash when you try to select a NUMERIC
|
|
value that has a value of NULL.
|
|
|
|
Added to support Pyodbc bug #351.
|
|
"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def text_type(self):
|
|
"""Target database must support an unbounded Text() "
|
|
"type such as TEXT or CLOB"""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def empty_strings_varchar(self):
|
|
"""target database can persist/return an empty string with a
|
|
varchar.
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def empty_strings_text(self):
|
|
"""target database can persist/return an empty string with an
|
|
unbounded text."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def expressions_against_unbounded_text(self):
|
|
"""target database supports use of an unbounded textual field in a
|
|
WHERE clause."""
|
|
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def selectone(self):
|
|
"""target driver must support the literal statement 'select 1'"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def savepoints(self):
|
|
"""Target database must support savepoints."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def two_phase_transactions(self):
|
|
"""Target database must support two-phase transactions."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def update_from(self):
|
|
"""Target must support UPDATE..FROM syntax"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def delete_from(self):
|
|
"""Target must support DELETE FROM..FROM or DELETE..USING syntax"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def update_where_target_in_subquery(self):
|
|
"""Target must support UPDATE (or DELETE) where the same table is
|
|
present in a subquery in the WHERE clause.
|
|
|
|
This is an ANSI-standard syntax that apparently MySQL can't handle,
|
|
such as::
|
|
|
|
UPDATE documents SET flag=1 WHERE documents.title IN
|
|
(SELECT max(documents.title) AS title
|
|
FROM documents GROUP BY documents.user_id
|
|
)
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def mod_operator_as_percent_sign(self):
|
|
"""target database must use a plain percent '%' as the 'modulus'
|
|
operator."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def percent_schema_names(self):
|
|
"""target backend supports weird identifiers with percent signs
|
|
in them, e.g. 'some % column'.
|
|
|
|
this is a very weird use case but often has problems because of
|
|
DBAPIs that use python formatting. It's not a critical use
|
|
case either.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def order_by_col_from_union(self):
|
|
"""target database supports ordering by a column from a SELECT
|
|
inside of a UNION
|
|
|
|
E.g. (SELECT id, ...) UNION (SELECT id, ...) ORDER BY id
|
|
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def order_by_label_with_expression(self):
|
|
"""target backend supports ORDER BY a column label within an
|
|
expression.
|
|
|
|
Basically this::
|
|
|
|
select data as foo from test order by foo || 'bar'
|
|
|
|
Lots of databases including PostgreSQL don't support this,
|
|
so this is off by default.
|
|
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def order_by_collation(self):
|
|
def check(config):
|
|
try:
|
|
self.get_order_by_collation(config)
|
|
return False
|
|
except NotImplementedError:
|
|
return True
|
|
|
|
return exclusions.skip_if(check)
|
|
|
|
def get_order_by_collation(self, config):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def unicode_connections(self):
|
|
"""Target driver must support non-ASCII characters being passed at
|
|
all.
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def graceful_disconnects(self):
|
|
"""Target driver must raise a DBAPI-level exception, such as
|
|
InterfaceError, when the underlying connection has been closed
|
|
and the execute() method is called.
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def independent_connections(self):
|
|
"""
|
|
Target must support simultaneous, independent database connections.
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def skip_mysql_on_windows(self):
|
|
"""Catchall for a large variety of MySQL on Windows failures"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def ad_hoc_engines(self):
|
|
"""Test environment must allow ad-hoc engine/connection creation.
|
|
|
|
DBs that scale poorly for many connections, even when closed, i.e.
|
|
Oracle, may use the "--low-connections" option which flags this
|
|
requirement as not present.
|
|
|
|
"""
|
|
return exclusions.skip_if(
|
|
lambda config: config.options.low_connections
|
|
)
|
|
|
|
@property
|
|
def no_windows(self):
|
|
return exclusions.skip_if(self._running_on_windows())
|
|
|
|
def _running_on_windows(self):
|
|
return exclusions.LambdaPredicate(
|
|
lambda: platform.system() == "Windows",
|
|
description="running on Windows",
|
|
)
|
|
|
|
@property
|
|
def timing_intensive(self):
|
|
return exclusions.requires_tag("timing_intensive")
|
|
|
|
@property
|
|
def memory_intensive(self):
|
|
return exclusions.requires_tag("memory_intensive")
|
|
|
|
@property
|
|
def threading_with_mock(self):
|
|
"""Mark tests that use threading and mock at the same time - stability
|
|
issues have been observed with coverage + python 3.3
|
|
|
|
"""
|
|
return exclusions.skip_if(
|
|
lambda config: util.py3k and config.options.has_coverage,
|
|
"Stability issues with coverage + py3k",
|
|
)
|
|
|
|
@property
|
|
def sqlalchemy2_stubs(self):
|
|
def check(config):
|
|
try:
|
|
__import__("sqlalchemy-stubs.ext.mypy")
|
|
except ImportError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
return exclusions.only_if(check)
|
|
|
|
@property
|
|
def python2(self):
|
|
return exclusions.skip_if(
|
|
lambda: sys.version_info >= (3,),
|
|
"Python version 2.xx is required.",
|
|
)
|
|
|
|
@property
|
|
def python3(self):
|
|
return exclusions.skip_if(
|
|
lambda: sys.version_info < (3,), "Python version 3.xx is required."
|
|
)
|
|
|
|
@property
|
|
def pep520(self):
|
|
return self.python36
|
|
|
|
@property
|
|
def insert_order_dicts(self):
|
|
return self.python37
|
|
|
|
@property
|
|
def python36(self):
|
|
return exclusions.skip_if(
|
|
lambda: sys.version_info < (3, 6),
|
|
"Python version 3.6 or greater is required.",
|
|
)
|
|
|
|
@property
|
|
def python37(self):
|
|
return exclusions.skip_if(
|
|
lambda: sys.version_info < (3, 7),
|
|
"Python version 3.7 or greater is required.",
|
|
)
|
|
|
|
@property
|
|
def dataclasses(self):
|
|
return self.python37
|
|
|
|
@property
|
|
def python38(self):
|
|
return exclusions.only_if(
|
|
lambda: util.py38, "Python 3.8 or above required"
|
|
)
|
|
|
|
@property
|
|
def cpython(self):
|
|
return exclusions.only_if(
|
|
lambda: util.cpython, "cPython interpreter needed"
|
|
)
|
|
|
|
@property
|
|
def patch_library(self):
|
|
def check_lib():
|
|
try:
|
|
__import__("patch")
|
|
except ImportError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
return exclusions.only_if(check_lib, "patch library needed")
|
|
|
|
@property
|
|
def non_broken_pickle(self):
|
|
from sqlalchemy.util import pickle
|
|
|
|
return exclusions.only_if(
|
|
lambda: util.cpython
|
|
and pickle.__name__ == "cPickle"
|
|
or sys.version_info >= (3, 2),
|
|
"Needs cPickle+cPython or newer Python 3 pickle",
|
|
)
|
|
|
|
@property
|
|
def predictable_gc(self):
|
|
"""target platform must remove all cycles unconditionally when
|
|
gc.collect() is called, as well as clean out unreferenced subclasses.
|
|
|
|
"""
|
|
return self.cpython
|
|
|
|
@property
|
|
def no_coverage(self):
|
|
"""Test should be skipped if coverage is enabled.
|
|
|
|
This is to block tests that exercise libraries that seem to be
|
|
sensitive to coverage, such as PostgreSQL notice logging.
|
|
|
|
"""
|
|
return exclusions.skip_if(
|
|
lambda config: config.options.has_coverage,
|
|
"Issues observed when coverage is enabled",
|
|
)
|
|
|
|
def _has_mysql_on_windows(self, config):
|
|
return False
|
|
|
|
def _has_mysql_fully_case_sensitive(self, config):
|
|
return False
|
|
|
|
@property
|
|
def sqlite(self):
|
|
return exclusions.skip_if(lambda: not self._has_sqlite())
|
|
|
|
@property
|
|
def cextensions(self):
|
|
return exclusions.skip_if(
|
|
lambda: not util.has_compiled_ext(), "C extensions not installed"
|
|
)
|
|
|
|
def _has_sqlite(self):
|
|
from sqlalchemy import create_engine
|
|
|
|
try:
|
|
create_engine("sqlite://")
|
|
return True
|
|
except ImportError:
|
|
return False
|
|
|
|
@property
|
|
def async_dialect(self):
|
|
"""dialect makes use of await_() to invoke operations on the DBAPI."""
|
|
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def asyncio(self):
|
|
return self.greenlet
|
|
|
|
@property
|
|
def greenlet(self):
|
|
def go(config):
|
|
try:
|
|
import greenlet # noqa: F401
|
|
except ImportError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
return exclusions.only_if(go)
|
|
|
|
@property
|
|
def computed_columns(self):
|
|
"Supports computed columns"
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def computed_columns_stored(self):
|
|
"Supports computed columns with `persisted=True`"
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def computed_columns_virtual(self):
|
|
"Supports computed columns with `persisted=False`"
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def computed_columns_default_persisted(self):
|
|
"""If the default persistence is virtual or stored when `persisted`
|
|
is omitted"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def computed_columns_reflect_persisted(self):
|
|
"""If persistence information is returned by the reflection of
|
|
computed columns"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def supports_distinct_on(self):
|
|
"""If a backend supports the DISTINCT ON in a select"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def supports_is_distinct_from(self):
|
|
"""Supports some form of "x IS [NOT] DISTINCT FROM y" construct.
|
|
Different dialects will implement their own flavour, e.g.,
|
|
sqlite will emit "x IS NOT y" instead of "x IS DISTINCT FROM y".
|
|
|
|
.. seealso::
|
|
|
|
:meth:`.ColumnOperators.is_distinct_from`
|
|
|
|
"""
|
|
return exclusions.skip_if(
|
|
lambda config: not config.db.dialect.supports_is_distinct_from,
|
|
"driver doesn't support an IS DISTINCT FROM construct",
|
|
)
|
|
|
|
@property
|
|
def identity_columns(self):
|
|
"""If a backend supports GENERATED { ALWAYS | BY DEFAULT }
|
|
AS IDENTITY"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def identity_columns_standard(self):
|
|
"""If a backend supports GENERATED { ALWAYS | BY DEFAULT }
|
|
AS IDENTITY with a standard syntax.
|
|
This is mainly to exclude MSSql.
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def regexp_match(self):
|
|
"""backend supports the regexp_match operator."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def regexp_replace(self):
|
|
"""backend supports the regexp_replace operator."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fetch_first(self):
|
|
"""backend supports the fetch first clause."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fetch_percent(self):
|
|
"""backend supports the fetch first clause with percent."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fetch_ties(self):
|
|
"""backend supports the fetch first clause with ties."""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fetch_no_order_by(self):
|
|
"""backend supports the fetch first without order by"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fetch_offset_with_options(self):
|
|
"""backend supports the offset when using fetch first with percent
|
|
or ties. basically this is "not mssql"
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def fetch_expression(self):
|
|
"""backend supports fetch / offset with expression in them, like
|
|
|
|
SELECT * FROM some_table
|
|
OFFSET 1 + 1 ROWS FETCH FIRST 1 + 1 ROWS ONLY
|
|
"""
|
|
return exclusions.closed()
|
|
|
|
@property
|
|
def autoincrement_without_sequence(self):
|
|
"""If autoincrement=True on a column does not require an explicit
|
|
sequence. This should be false only for oracle.
|
|
"""
|
|
return exclusions.open()
|
|
|
|
@property
|
|
def generic_classes(self):
|
|
"If X[Y] can be implemented with ``__class_getitem__``. py3.7+"
|
|
return exclusions.only_if(lambda: util.py37)
|