Source

sqlalchemy / test / requirements.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
"""Requirements specific to SQLAlchemy's own unit tests.


"""

from sqlalchemy import util
import sys
from sqlalchemy.testing.requirements import SuiteRequirements
from sqlalchemy.testing import exclusions
from sqlalchemy.testing.exclusions import \
     skip, \
     skip_if,\
     only_if,\
     only_on,\
     fails_on_everything_except,\
     fails_if,\
     succeeds_if,\
     SpecPredicate,\
     against

def no_support(db, reason):
    return SpecPredicate(db, description=reason)

def exclude(db, op, spec, description=None):
    return SpecPredicate(db, op, spec, description=description)

class DefaultRequirements(SuiteRequirements):
    @property
    def deferrable_or_no_constraints(self):
        """Target database must support derferable constraints."""

        return skip_if([
            no_support('firebird', 'not supported by database'),
            no_support('mysql', 'not supported by database'),
            no_support('mssql', 'not supported by database'),
            ])

    @property
    def named_constraints(self):
        """target database must support names for constraints."""

        return skip_if([
            no_support('sqlite', 'not supported by database'),
            ])

    @property
    def foreign_keys(self):
        """Target database must support foreign keys."""

        return skip_if(
                no_support('sqlite', 'not supported by database')
            )

    @property
    def unbounded_varchar(self):
        """Target database must support VARCHAR with no length"""

        return skip_if([
                "firebird", "oracle", "mysql"
            ], "not supported by database"
            )

    @property
    def boolean_col_expressions(self):
        """Target database must support boolean expressions as columns"""
        return skip_if([
            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'),
        ])

    @property
    def standalone_binds(self):
        """target database/driver supports bound parameters as column expressions
        without being in the context of a typed column.

        """
        return skip_if(["firebird", "mssql+mxodbc"],
                "not supported by driver")

    @property
    def identity(self):
        """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 skip_if(["firebird", "oracle", "postgresql", "sybase"],
                "not supported by database"
            )

    @property
    def reflectable_autoincrement(self):
        """Target database must support tables that can automatically generate
        PKs assuming they were reflected.

        this is essentially all the DBs in "identity" plus Postgresql, which
        has SERIAL support.  FB and Oracle (and sybase?) require the Sequence to
        be explicitly added, including if the table was reflected.
        """
        return skip_if(["firebird", "oracle", "sybase"],
                "not supported by database"
            )

    @property
    def binary_comparisons(self):
        """target database/driver can allow BLOB/BINARY fields to be compared
        against a bound parameter value.
        """
        return skip_if(["oracle", "mssql"],
                "not supported by database/driver"
            )

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

        return skip_if(["mssql+pyodbc", "mssql+mxodbc"], "no driver support")

    @property
    def independent_connections(self):
        """Target must support simultaneous, independent database connections."""

        # This is also true of some configurations of UnixODBC and probably win32
        # ODBC as well.
        return skip_if([
                    no_support("sqlite",
                            "independent connections disabled "
                                "when :memory: connections are used"),
                    exclude("mssql", "<", (9, 0, 0),
                            "SQL Server 2005+ is required for "
                                "independent connections"
                        )
                    ]
                )

    @property
    def updateable_autoincrement_pks(self):
        """Target must support UPDATE on autoincrement/integer primary key."""

        return skip_if(["mssql", "sybase"],
                "IDENTITY columns can't be updated")

    @property
    def isolation_level(self):
        return only_on(
                    ('postgresql', 'sqlite', 'mysql'),
                    "DBAPI has no isolation level support"
                ).fails_on('postgresql+pypostgresql',
                          'pypostgresql bombs on multiple isolation level calls')

    @property
    def row_triggers(self):
        """Target must support standard statement-running EACH ROW triggers."""

        return skip_if([
            # 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'),
        ])

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

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

    @property
    def update_from(self):
        """Target must support UPDATE..FROM syntax"""

        return only_on(['postgresql', 'mssql', 'mysql'],
                "Backend does not support UPDATE..FROM")


    @property
    def update_where_target_in_subquery(self):
        """Target must support UPDATE 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 fails_if('mysql', 'MySQL error 1093 "Cant specify target table '
                                        'for update in FROM clause"')

    @property
    def savepoints(self):
        """Target database must support savepoints."""

        return skip_if([
                    "sqlite",
                    "sybase",
                    ("mysql", "<", (5, 0, 3)),
                    ("informix", "<", (11, 55, "xC3"))
                    ], "savepoints not supported")


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

        return skip_if([
                    "sqlite",
                    "firebird"
                ], "no schema support")


    @property
    def update_nowait(self):
        """Target database must support SELECT...FOR UPDATE NOWAIT"""
        return skip_if(["firebird", "mssql", "mysql", "sqlite", "sybase"],
                "no FOR UPDATE NOWAIT support"
            )

    @property
    def subqueries(self):
        """Target database must support subqueries."""

        return skip_if(exclude('mysql', '<', (4, 1, 1)), 'no subquery support')

    @property
    def intersect(self):
        """Target database must support INTERSECT or equivalent."""

        return fails_if([
                "firebird", "mysql", "sybase", "informix"
            ], 'no support for INTERSECT')

    @property
    def except_(self):
        """Target database must support EXCEPT or equivalent (i.e. MINUS)."""
        return fails_if([
                "firebird", "mysql", "sybase", "informix"
            ], 'no support for EXCEPT')

    @property
    def offset(self):
        """Target database must support some method of adding OFFSET or
        equivalent to a result set."""
        return fails_if([
                "sybase"
            ], 'no support for OFFSET or equivalent')

    @property
    def window_functions(self):
        return only_if([
                    "postgresql", "mssql", "oracle"
                ], "Backend does not support window functions")

    @property
    def two_phase_transactions(self):
        """Target database must support two-phase transactions."""

        return skip_if([
            no_support('firebird', 'no SA implementation'),
            no_support('maxdb', 'two-phase xact not supported by database'),
            no_support('mssql', 'two-phase xact not supported by drivers'),
            no_support('oracle', 'two-phase xact not implemented in SQLA/oracle'),
            no_support('drizzle', 'two-phase xact not supported by database'),
            no_support('sqlite', 'two-phase xact not supported by database'),
            no_support('sybase', 'two-phase xact not supported by drivers/SQLA'),
            no_support('postgresql+zxjdbc',
                    'FIXME: JDBC driver confuses the transaction state, may '
                       'need separate XA implementation'),
            exclude('mysql', '<', (5, 0, 3),
                        'two-phase xact not supported by database'),
            ])

    @property
    def views(self):
        """Target database must support VIEWs."""

        return skip_if("drizzle", "no VIEW support")

    @property
    def empty_strings_varchar(self):
        """target database can persist/return an empty string with a varchar."""

        return fails_if(["oracle"],
                        'oracle converts empty strings to a blank space')

    @property
    def empty_strings_text(self):
        """target database can persist/return an empty string with an
        unbounded text."""

        return exclusions.open()

    @property
    def unicode_data(self):
        return skip_if([
            no_support("sybase", "no unicode driver support")
            ])

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

    @property
    def unicode_ddl(self):
        """Target driver must support some encoding of Unicode across the wire."""
        # TODO: expand to exclude MySQLdb versions w/ broken unicode
        return skip_if([
            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'),
            ])

    @property
    def sane_rowcount(self):
        return skip_if(
            lambda: not self.db.dialect.supports_sane_rowcount,
            "driver doesn't support 'sane' rowcount"
        )

    @property
    def cextensions(self):
        return skip_if(
                lambda: not self._has_cextensions(), "C extensions not installed"
                )

    @property
    def emulated_lastrowid(self):
        """"target dialect retrieves cursor.lastrowid or an equivalent
        after an insert() construct executes.
        """
        return fails_on_everything_except('mysql+mysqldb', 'mysql+oursql',
                                      'sqlite+pysqlite', 'mysql+pymysql',
                                      'sybase', 'mssql+pyodbc', 'mssql+mxodbc')

    @property
    def implements_get_lastrowid(self):
        return skip_if([
            no_support('sybase', 'not supported by database'),
            ])

    @property
    def dbapi_lastrowid(self):
        """"target backend includes a 'lastrowid' accessor on the DBAPI
        cursor object.

        """
        return fails_on_everything_except('mysql+mysqldb', 'mysql+oursql',
                                       'sqlite+pysqlite', 'mysql+pymysql')

    @property
    def sane_multi_rowcount(self):
        return skip_if(
                    lambda: not self.db.dialect.supports_sane_multi_rowcount,
                    "driver doesn't support 'sane' multi row count"
                )

    @property
    def nullsordering(self):
        """Target backends that support nulls ordering."""
        return fails_on_everything_except('postgresql', 'oracle', 'firebird')

    @property
    def reflects_pk_names(self):
        """Target driver reflects the name of primary key constraints."""

        return fails_on_everything_except('postgresql', 'oracle', 'mssql',
                    'sybase')

    @property
    def datetime(self):
        """target dialect supports representation of Python
        datetime.datetime() objects."""

        return exclusions.open()

    @property
    def datetime_microseconds(self):
        """target dialect supports representation of Python
        datetime.datetime() with microsecond objects."""

        return skip_if(['mssql', 'mysql', 'firebird', '+zxjdbc',
                    'oracle', 'sybase'])

    @property
    def datetime_historic(self):
        """target dialect supports representation of Python
        datetime.datetime() objects with historic (pre 1900) values."""

        return succeeds_if(['sqlite', 'postgresql', 'firebird'])

    @property
    def date(self):
        """target dialect supports representation of Python
        datetime.date() objects."""

        return exclusions.open()

    @property
    def date_historic(self):
        """target dialect supports representation of Python
        datetime.datetime() objects with historic (pre 1900) values."""

        return succeeds_if(['sqlite', 'postgresql', 'firebird'])

    @property
    def time(self):
        """target dialect supports representation of Python
        datetime.time() objects."""

        return skip_if(['oracle'])

    @property
    def time_microseconds(self):
        """target dialect supports representation of Python
        datetime.time() with microsecond objects."""

        return skip_if(['mssql', 'mysql', 'firebird', '+zxjdbc',
                    'oracle', 'sybase'])

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

    @property
    def python3(self):
        return skip_if(
                lambda: sys.version_info < (3,),
                "Python version 3.xx is required."
                )

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

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

    @property
    def cpython(self):
        return only_if(lambda: util.cpython,
               "cPython interpreter needed"
             )

    @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 hstore(self):
        def check_hstore():
            if not against("postgresql"):
                return False
            try:
                self.db.execute("SELECT 'a=>1,a=>2'::hstore;")
                return True
            except:
                return False

        return only_if(check_hstore)

    @property
    def sqlite(self):
        return skip_if(lambda: not self._has_sqlite())

    @property
    def oracle_test_dblink(self):
        return skip_if(
                    lambda: not self.config.file_config.has_option(
                        'sqla_testing', 'oracle_db_link'),
                    "oracle_db_link option not specified in config"
                )

    @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 skip_if(lambda: self.config.options.low_connections)

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

        return skip_if(self._has_mysql_on_windows,
                "Not supported on MySQL + Windows"
            )

    @property
    def english_locale_on_postgresql(self):
        return skip_if(lambda: against('postgresql') \
                    and not self.db.scalar('SHOW LC_COLLATE').startswith('en'))

    @property
    def selectone(self):
        """target driver must support the literal statement 'select 1'"""
        return skip_if(["oracle", "firebird"], "non-standard SELECT scalar syntax")

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

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

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

    def _has_mysql_fully_case_sensitive(self):
        return against('mysql') and \
                self.db.dialect._detect_casing(self.db) == 0