1"""Requirements specific to SQLAlchemy's own unit tests.
2
3
4"""
5
6import sys
7
8from sqlalchemy import exc
9from sqlalchemy.sql import text
10from sqlalchemy.testing import exclusions
11from sqlalchemy.testing.exclusions import against
12from sqlalchemy.testing.exclusions import fails_if
13from sqlalchemy.testing.exclusions import fails_on
14from sqlalchemy.testing.exclusions import fails_on_everything_except
15from sqlalchemy.testing.exclusions import LambdaPredicate
16from sqlalchemy.testing.exclusions import NotPredicate
17from sqlalchemy.testing.exclusions import only_if
18from sqlalchemy.testing.exclusions import only_on
19from sqlalchemy.testing.exclusions import skip_if
20from sqlalchemy.testing.exclusions import SpecPredicate
21from sqlalchemy.testing.exclusions import succeeds_if
22from sqlalchemy.testing.requirements import SuiteRequirements
23
24
25def no_support(db, reason):
26    return SpecPredicate(db, description=reason)
27
28
29def exclude(db, op, spec, description=None):
30    return SpecPredicate(db, op, spec, description=description)
31
32
33class DefaultRequirements(SuiteRequirements):
34    @property
35    def deferrable_or_no_constraints(self):
36        """Target database must support deferrable constraints."""
37
38        return skip_if(
39            [
40                no_support("mysql", "not supported by database"),
41                no_support("mariadb", "not supported by database"),
42                no_support("mssql", "not supported by database"),
43            ]
44        )
45
46    @property
47    def check_constraints(self):
48        """Target database must support check constraints."""
49
50        return exclusions.open()
51
52    @property
53    def enforces_check_constraints(self):
54        """Target database must also enforce check constraints."""
55
56        return self.check_constraints + fails_on(
57            self._mysql_check_constraints_dont_exist,
58            "check constraints don't enforce on MySQL, MariaDB<10.2",
59        )
60
61    @property
62    def named_constraints(self):
63        """target database must support names for constraints."""
64
65        return exclusions.open()
66
67    @property
68    def implicitly_named_constraints(self):
69        """target database must apply names to unnamed constraints."""
70
71        return skip_if([no_support("sqlite", "not supported by database")])
72
73    @property
74    def foreign_keys(self):
75        """Target database must support foreign keys."""
76
77        return skip_if(no_support("sqlite", "not supported by database"))
78
79    @property
80    def foreign_key_constraint_name_reflection(self):
81        return fails_if(
82            lambda config: against(config, ["mysql", "mariadb"])
83            and not self._mysql_80(config)
84            and not self._mariadb_105(config)
85        )
86
87    @property
88    def table_ddl_if_exists(self):
89        """target platform supports IF NOT EXISTS / IF EXISTS for tables."""
90
91        return only_on(["postgresql", "mysql", "mariadb", "sqlite"])
92
93    @property
94    def index_ddl_if_exists(self):
95        """target platform supports IF NOT EXISTS / IF EXISTS for indexes."""
96
97        # mariadb but not mysql, tested up to mysql 8
98        return only_on(["postgresql", "mariadb", "sqlite"])
99
100    @property
101    def on_update_cascade(self):
102        """target database must support ON UPDATE..CASCADE behavior in
103        foreign keys."""
104
105        return skip_if(
106            ["sqlite", "oracle"],
107            "target backend %(doesnt_support)s ON UPDATE CASCADE",
108        )
109
110    @property
111    def non_updating_cascade(self):
112        """target database must *not* support ON UPDATE..CASCADE behavior in
113        foreign keys."""
114
115        return fails_on_everything_except("sqlite", "oracle") + skip_if(
116            "mssql"
117        )
118
119    @property
120    def recursive_fk_cascade(self):
121        """target database must support ON DELETE CASCADE on a self-referential
122        foreign key"""
123
124        return skip_if(["mssql"])
125
126    @property
127    def deferrable_fks(self):
128        """target database must support deferrable fks"""
129
130        return only_on(["oracle", "postgresql"])
131
132    @property
133    def foreign_key_constraint_option_reflection_ondelete(self):
134        return only_on(
135            ["postgresql", "mysql", "mariadb", "sqlite", "oracle", "mssql"]
136        )
137
138    @property
139    def fk_constraint_option_reflection_ondelete_restrict(self):
140        return only_on(["postgresql", "sqlite", self._mysql_80])
141
142    @property
143    def fk_constraint_option_reflection_ondelete_noaction(self):
144        return only_on(["postgresql", "mysql", "mariadb", "sqlite", "mssql"])
145
146    @property
147    def foreign_key_constraint_option_reflection_onupdate(self):
148        return only_on(["postgresql", "mysql", "mariadb", "sqlite", "mssql"])
149
150    @property
151    def fk_constraint_option_reflection_onupdate_restrict(self):
152        return only_on(["postgresql", "sqlite", self._mysql_80])
153
154    @property
155    def comment_reflection(self):
156        return only_on(["postgresql", "mysql", "mariadb", "oracle"])
157
158    @property
159    def unbounded_varchar(self):
160        """Target database must support VARCHAR with no length"""
161
162        return skip_if(
163            ["oracle", "mysql", "mariadb"],
164            "not supported by database",
165        )
166
167    @property
168    def boolean_col_expressions(self):
169        """Target database must support boolean expressions as columns"""
170        return skip_if(
171            [
172                no_support("oracle", "not supported by database"),
173                no_support("mssql", "not supported by database"),
174            ]
175        )
176
177    @property
178    def non_native_boolean_unconstrained(self):
179        """target database is not native boolean and allows arbitrary integers
180        in it's "bool" column"""
181
182        return skip_if(
183            [
184                LambdaPredicate(
185                    lambda config: against(config, "mssql"),
186                    "SQL Server drivers / odbc seem to change "
187                    "their mind on this",
188                ),
189                LambdaPredicate(
190                    lambda config: config.db.dialect.supports_native_boolean,
191                    "native boolean dialect",
192                ),
193            ]
194        )
195
196    @property
197    def qmark_paramstyle(self):
198        return only_on(["sqlite", "+pyodbc"])
199
200    @property
201    def named_paramstyle(self):
202        return only_on(["sqlite", "oracle+cx_oracle"])
203
204    @property
205    def format_paramstyle(self):
206        return only_on(
207            [
208                "mysql+mysqldb",
209                "mysql+pymysql",
210                "mysql+cymysql",
211                "mysql+mysqlconnector",
212                "mariadb+mysqldb",
213                "mariadb+pymysql",
214                "mariadb+cymysql",
215                "mariadb+mysqlconnector",
216                "postgresql+pg8000",
217            ]
218        )
219
220    @property
221    def pyformat_paramstyle(self):
222        return only_on(
223            [
224                "postgresql+psycopg2",
225                "postgresql+psycopg2cffi",
226                "mysql+mysqlconnector",
227                "mysql+pymysql",
228                "mysql+cymysql",
229                "mariadb+mysqlconnector",
230                "mariadb+pymysql",
231                "mariadb+cymysql",
232                "mssql+pymssql",
233            ]
234        )
235
236    @property
237    def no_quoting_special_bind_names(self):
238        """Target database will quote bound parameter names, doesn't support
239        EXPANDING"""
240
241        return skip_if(["oracle"])
242
243    @property
244    def temporary_tables(self):
245        """target database supports temporary tables"""
246        return skip_if([self._sqlite_file_db], "not supported (?)")
247
248    @property
249    def temp_table_reflection(self):
250        return self.temporary_tables
251
252    @property
253    def temp_table_reflect_indexes(self):
254        return skip_if(["mssql", self._sqlite_file_db], "not supported (?)")
255
256    @property
257    def reflectable_autoincrement(self):
258        """Target database must support tables that can automatically generate
259        PKs assuming they were reflected.
260
261        this is essentially all the DBs in "identity" plus PostgreSQL, which
262        has SERIAL support.  Oracle requires the Sequence
263        to be explicitly added, including if the table was reflected.
264        """
265        return skip_if(["oracle"], "not supported by database")
266
267    @property
268    def non_broken_binary(self):
269        """target DBAPI must work fully with binary values"""
270
271        # see https://github.com/pymssql/pymssql/issues/504
272        return skip_if(["mssql+pymssql"])
273
274    @property
275    def binary_comparisons(self):
276        """target database/driver can allow BLOB/BINARY fields to be compared
277        against a bound parameter value.
278        """
279        return skip_if(["oracle", "mssql"], "not supported by database/driver")
280
281    @property
282    def binary_literals(self):
283        """target backend supports simple binary literals, e.g. an
284        expression like::
285
286            SELECT CAST('foo' AS BINARY)
287
288        Where ``BINARY`` is the type emitted from :class:`.LargeBinary`,
289        e.g. it could be ``BLOB`` or similar.
290
291        Basically fails on Oracle.
292
293        """
294        # adding mssql here since it doesn't support comparisons either,
295        # have observed generally bad behavior with binary / mssql.
296
297        return skip_if(["oracle", "mssql"], "not supported by database/driver")
298
299    @property
300    def tuple_in(self):
301        def _sqlite_tuple_in(config):
302            return against(
303                config, "sqlite"
304            ) and config.db.dialect.dbapi.sqlite_version_info >= (3, 15, 0)
305
306        return only_on(
307            ["mysql", "mariadb", "postgresql", _sqlite_tuple_in, "oracle"]
308        )
309
310    @property
311    def tuple_in_w_empty(self):
312        return self.tuple_in + skip_if(["oracle"])
313
314    @property
315    def independent_cursors(self):
316        """Target must support simultaneous, independent database cursors
317        on a single connection."""
318
319        return skip_if(["mssql", "mysql", "mariadb"], "no driver support")
320
321    @property
322    def cursor_works_post_rollback(self):
323        """Driver quirk where the cursor.fetchall() will work even if
324        the connection has been rolled back.
325
326        This generally refers to buffered cursors but also seems to work
327        with cx_oracle, for example.
328
329        """
330
331        return skip_if(["+pyodbc"], "no driver support")
332
333    @property
334    def independent_connections(self):
335        """
336        Target must support simultaneous, independent database connections.
337        """
338
339        # This is also true of some configurations of UnixODBC and probably
340        # win32 ODBC as well.
341        return skip_if(
342            [
343                no_support(
344                    "sqlite",
345                    "independent connections disabled "
346                    "when :memory: connections are used",
347                ),
348                exclude(
349                    "mssql",
350                    "<",
351                    (9, 0, 0),
352                    "SQL Server 2005+ is required for "
353                    "independent connections",
354                ),
355            ]
356        )
357
358    @property
359    def memory_process_intensive(self):
360        """Driver is able to handle the memory tests which run in a subprocess
361        and iterate through hundreds of connections
362
363        """
364        return skip_if(
365            [
366                no_support("oracle", "Oracle XE usually can't handle these"),
367                no_support("mssql+pyodbc", "MS ODBC drivers struggle"),
368                self._running_on_windows(),
369            ]
370        )
371
372    @property
373    def updateable_autoincrement_pks(self):
374        """Target must support UPDATE on autoincrement/integer primary key."""
375
376        return skip_if(["mssql"], "IDENTITY columns can't be updated")
377
378    @property
379    def isolation_level(self):
380        return only_on(
381            ("postgresql", "sqlite", "mysql", "mariadb", "mssql", "oracle"),
382            "DBAPI has no isolation level support",
383        )
384
385    @property
386    def legacy_isolation_level(self):
387        # refers to the engine isolation_level setting
388        return only_on(
389            ("postgresql", "sqlite", "mysql", "mariadb", "mssql"),
390            "DBAPI has no isolation level support",
391        )
392
393    def get_isolation_levels(self, config):
394        levels = set(config.db.dialect._isolation_lookup)
395
396        if against(config, "sqlite"):
397            default = "SERIALIZABLE"
398            levels.add("AUTOCOMMIT")
399        elif against(config, "postgresql"):
400            default = "READ COMMITTED"
401            levels.add("AUTOCOMMIT")
402        elif against(config, "mysql"):
403            default = "REPEATABLE READ"
404            levels.add("AUTOCOMMIT")
405        elif against(config, "mariadb"):
406            default = "REPEATABLE READ"
407            levels.add("AUTOCOMMIT")
408        elif against(config, "mssql"):
409            default = "READ COMMITTED"
410            levels.add("AUTOCOMMIT")
411        elif against(config, "oracle"):
412            default = "READ COMMITTED"
413            levels.add("AUTOCOMMIT")
414        else:
415            raise NotImplementedError()
416
417        return {"default": default, "supported": levels}
418
419    @property
420    def autocommit(self):
421        """target dialect supports 'AUTOCOMMIT' as an isolation_level"""
422
423        return self.isolation_level + only_if(
424            lambda config: "AUTOCOMMIT"
425            in self.get_isolation_levels(config)["supported"]
426        )
427
428    @property
429    def row_triggers(self):
430        """Target must support standard statement-running EACH ROW triggers."""
431
432        return skip_if(
433            [
434                # no access to same table
435                no_support("mysql", "requires SUPER priv"),
436                no_support("mariadb", "requires SUPER priv"),
437                exclude("mysql", "<", (5, 0, 10), "not supported by database"),
438            ]
439        )
440
441    @property
442    def sequences_as_server_defaults(self):
443        """Target database must support SEQUENCE as a server side default."""
444
445        return self.sequences + only_on(
446            ["postgresql", "mariadb", "oracle >= 18"],
447            "doesn't support sequences as a server side default.",
448        )
449
450    @property
451    def sql_expressions_inserted_as_primary_key(self):
452        return only_if([self.returning, self.sqlite])
453
454    @property
455    def computed_columns_on_update_returning(self):
456        return self.computed_columns + skip_if("oracle")
457
458    @property
459    def correlated_outer_joins(self):
460        """Target must support an outer join to a subquery which
461        correlates to the parent."""
462
463        return skip_if(
464            "oracle",
465            'Raises "ORA-01799: a column may not be '
466            'outer-joined to a subquery"',
467        )
468
469    @property
470    def multi_table_update(self):
471        return only_on(["mysql", "mariadb"], "Multi table update")
472
473    @property
474    def update_from(self):
475        """Target must support UPDATE..FROM syntax"""
476
477        return only_on(
478            ["postgresql", "mssql", "mysql", "mariadb"],
479            "Backend does not support UPDATE..FROM",
480        )
481
482    @property
483    def delete_from(self):
484        """Target must support DELETE FROM..FROM or DELETE..USING syntax"""
485        return only_on(
486            ["postgresql", "mssql", "mysql", "mariadb"],
487            "Backend does not support DELETE..FROM",
488        )
489
490    @property
491    def update_where_target_in_subquery(self):
492        """Target must support UPDATE (or DELETE) where the same table is
493        present in a subquery in the WHERE clause.
494
495        This is an ANSI-standard syntax that apparently MySQL can't handle,
496        such as::
497
498            UPDATE documents SET flag=1 WHERE documents.title IN
499                (SELECT max(documents.title) AS title
500                    FROM documents GROUP BY documents.user_id
501                )
502
503        """
504        return fails_if(
505            self._mysql_not_mariadb_103,
506            'MySQL error 1093 "Cant specify target table '
507            'for update in FROM clause", resolved by MariaDB 10.3',
508        )
509
510    @property
511    def savepoints(self):
512        """Target database must support savepoints."""
513
514        return skip_if(
515            ["sqlite", ("mysql", "<", (5, 0, 3))],
516            "savepoints not supported",
517        )
518
519    @property
520    def savepoints_w_release(self):
521        return self.savepoints + skip_if(
522            ["oracle", "mssql"],
523            "database doesn't support release of savepoint",
524        )
525
526    @property
527    def schemas(self):
528        """Target database must support external schemas, and have one
529        named 'test_schema'."""
530
531        return exclusions.open()
532
533    @property
534    def cross_schema_fk_reflection(self):
535        """target system must support reflection of inter-schema foreign
536        keys"""
537        return only_on(["postgresql", "mysql", "mariadb", "mssql"])
538
539    @property
540    def implicit_default_schema(self):
541        """target system has a strong concept of 'default' schema that can
542        be referred to implicitly.
543
544        basically, PostgreSQL.
545
546        """
547        return only_on(["postgresql"])
548
549    @property
550    def default_schema_name_switch(self):
551        return only_on(["postgresql", "oracle"])
552
553    @property
554    def unique_constraint_reflection(self):
555        return fails_on_everything_except(
556            "postgresql", "mysql", "mariadb", "sqlite", "oracle"
557        )
558
559    @property
560    def unique_constraint_reflection_no_index_overlap(self):
561        return (
562            self.unique_constraint_reflection
563            + skip_if("mysql")
564            + skip_if("mariadb")
565            + skip_if("oracle")
566        )
567
568    @property
569    def check_constraint_reflection(self):
570        return fails_on_everything_except(
571            "postgresql",
572            "sqlite",
573            "oracle",
574            self._mysql_and_check_constraints_exist,
575        )
576
577    @property
578    def indexes_with_expressions(self):
579        return only_on(["postgresql", "sqlite>=3.9.0"])
580
581    @property
582    def temp_table_names(self):
583        """target dialect supports listing of temporary table names"""
584
585        return only_on(["sqlite", "oracle"]) + skip_if(self._sqlite_file_db)
586
587    @property
588    def temporary_views(self):
589        """target database supports temporary views"""
590        return only_on(["sqlite", "postgresql"]) + skip_if(
591            self._sqlite_file_db
592        )
593
594    @property
595    def table_value_constructor(self):
596        return only_on(["postgresql", "mssql"])
597
598    @property
599    def update_nowait(self):
600        """Target database must support SELECT...FOR UPDATE NOWAIT"""
601        return skip_if(
602            ["mssql", "mysql", "mariadb<10.3", "sqlite"],
603            "no FOR UPDATE NOWAIT support",
604        )
605
606    @property
607    def subqueries(self):
608        """Target database must support subqueries."""
609        return exclusions.open()
610
611    @property
612    def ctes(self):
613        """Target database supports CTEs"""
614        return only_on(
615            [
616                lambda config: against(config, "mysql")
617                and (
618                    (
619                        config.db.dialect._is_mariadb
620                        and config.db.dialect._mariadb_normalized_version_info
621                        >= (10, 2)
622                    )
623                    or (
624                        not config.db.dialect._is_mariadb
625                        and config.db.dialect.server_version_info >= (8,)
626                    )
627                ),
628                "mariadb>10.2",
629                "postgresql",
630                "mssql",
631                "oracle",
632                "sqlite>=3.8.3",
633            ]
634        )
635
636    @property
637    def ctes_with_update_delete(self):
638        """target database supports CTES that ride on top of a normal UPDATE
639        or DELETE statement which refers to the CTE in a correlated subquery.
640
641        """
642        return only_on(
643            [
644                "postgresql",
645                "mssql",
646                # "oracle" - oracle can do this but SQLAlchemy doesn't support
647                # their syntax yet
648            ]
649        )
650
651    @property
652    def ctes_on_dml(self):
653        """target database supports CTES which consist of INSERT, UPDATE
654        or DELETE *within* the CTE, e.g. WITH x AS (UPDATE....)"""
655
656        return only_if(["postgresql"])
657
658    @property
659    def mod_operator_as_percent_sign(self):
660        """target database must use a plain percent '%' as the 'modulus'
661        operator."""
662
663        return only_if(
664            ["mysql", "mariadb", "sqlite", "postgresql+psycopg2", "mssql"]
665        )
666
667    @property
668    def intersect(self):
669        """Target database must support INTERSECT or equivalent."""
670
671        return fails_if(
672            [self._mysql_not_mariadb_103],
673            "no support for INTERSECT",
674        )
675
676    @property
677    def except_(self):
678        """Target database must support EXCEPT or equivalent (i.e. MINUS)."""
679        return fails_if([self._mysql_not_mariadb_103], "no support for EXCEPT")
680
681    @property
682    def dupe_order_by_ok(self):
683        """target db wont choke if ORDER BY specifies the same expression
684        more than once
685
686        """
687
688        return skip_if("mssql")
689
690    @property
691    def order_by_col_from_union(self):
692        """target database supports ordering by a column from a SELECT
693        inside of a UNION
694
695        E.g.  (SELECT id, ...) UNION (SELECT id, ...) ORDER BY id
696
697        Fails on SQL Server
698
699        """
700        return fails_if("mssql")
701
702    @property
703    def parens_in_union_contained_select_w_limit_offset(self):
704        """Target database must support parenthesized SELECT in UNION
705        when LIMIT/OFFSET is specifically present.
706
707        E.g. (SELECT ... LIMIT ..) UNION (SELECT .. OFFSET ..)
708
709        This is known to fail on SQLite.
710
711        """
712        return fails_if("sqlite")
713
714    @property
715    def parens_in_union_contained_select_wo_limit_offset(self):
716        """Target database must support parenthesized SELECT in UNION
717        when OFFSET/LIMIT is specifically not present.
718
719        E.g. (SELECT ...) UNION (SELECT ..)
720
721        This is known to fail on SQLite.  It also fails on Oracle
722        because without LIMIT/OFFSET, there is currently no step that
723        creates an additional subquery.
724
725        """
726        return fails_if(["sqlite", "oracle"])
727
728    @property
729    def sql_expression_limit_offset(self):
730        return (
731            fails_if(
732                ["mysql", "mariadb"],
733                "Target backend can't accommodate full expressions in "
734                "OFFSET or LIMIT",
735            )
736            + self.offset
737        )
738
739    @property
740    def window_functions(self):
741        return only_if(
742            [
743                "postgresql>=8.4",
744                "mssql",
745                "oracle",
746                "sqlite>=3.25.0",
747                "mysql>=8",
748                "mariadb>=10.2",
749            ],
750            "Backend does not support window functions",
751        )
752
753    @property
754    def two_phase_transactions(self):
755        """Target database must support two-phase transactions."""
756
757        def pg_prepared_transaction(config):
758            if not against(config, "postgresql"):
759                return True
760
761            with config.db.connect() as conn:
762                try:
763                    num = conn.scalar(
764                        text(
765                            "select cast(setting AS integer) from pg_settings "
766                            "where name = 'max_prepared_transactions'"
767                        )
768                    )
769                except exc.OperationalError:
770                    return False
771                else:
772                    return num > 0
773
774        return skip_if(
775            [
776                no_support("mssql", "two-phase xact not supported by drivers"),
777                no_support(
778                    "sqlite", "two-phase xact not supported by database"
779                ),
780                # in Ia3cbbf56d4882fcc7980f90519412f1711fae74d
781                # we are evaluating which modern MySQL / MariaDB versions
782                # can handle two-phase testing without too many problems
783                # no_support(
784                #     "mysql",
785                #    "recent MySQL communiity editions have too many issues "
786                #    "(late 2016), disabling for now",
787                # ),
788                NotPredicate(
789                    LambdaPredicate(
790                        pg_prepared_transaction,
791                        "max_prepared_transactions not available or zero",
792                    )
793                ),
794            ]
795        )
796
797    @property
798    def two_phase_recovery(self):
799        return self.two_phase_transactions + (
800            skip_if(
801                ["mysql", "mariadb"],
802                "still can't get recover to work w/ MariaDB / MySQL",
803            )
804            + skip_if("oracle", "recovery not functional")
805        )
806
807    @property
808    def views(self):
809        """Target database must support VIEWs."""
810
811        return skip_if("drizzle", "no VIEW support")
812
813    @property
814    def empty_strings_varchar(self):
815        """
816        target database can persist/return an empty string with a varchar.
817        """
818
819        return fails_if(
820            ["oracle"], "oracle converts empty strings to a blank space"
821        )
822
823    @property
824    def empty_strings_text(self):
825        """target database can persist/return an empty string with an
826        unbounded text."""
827
828        return fails_if(
829            ["oracle"], "oracle converts empty strings to a blank space"
830        )
831
832    @property
833    def empty_inserts_executemany(self):
834        # waiting on https://jira.mariadb.org/browse/CONPY-152
835        return skip_if(["mariadb+mariadbconnector"]) + self.empty_inserts
836
837    @property
838    def expressions_against_unbounded_text(self):
839        """target database supports use of an unbounded textual field in a
840        WHERE clause."""
841
842        return fails_if(
843            ["oracle"],
844            "ORA-00932: inconsistent datatypes: expected - got CLOB",
845        )
846
847    @property
848    def unicode_connections(self):
849        """
850        Target driver must support some encoding of Unicode across the wire.
851
852        """
853        return exclusions.open()
854
855    @property
856    def unicode_ddl(self):
857        """Target driver must support some degree of non-ascii symbol names."""
858
859        return skip_if(
860            [
861                no_support("mssql+pymssql", "no FreeTDS support"),
862            ]
863        )
864
865    @property
866    def symbol_names_w_double_quote(self):
867        """Target driver can create tables with a name like 'some " table'"""
868
869        return skip_if(
870            [no_support("oracle", "ORA-03001: unimplemented feature")]
871        )
872
873    @property
874    def emulated_lastrowid(self):
875        """ "target dialect retrieves cursor.lastrowid or an equivalent
876        after an insert() construct executes.
877        """
878        return fails_on_everything_except(
879            "mysql",
880            "mariadb",
881            "sqlite+aiosqlite",
882            "sqlite+pysqlite",
883            "sqlite+pysqlcipher",
884            "mssql",
885        )
886
887    @property
888    def emulated_lastrowid_even_with_sequences(self):
889        """ "target dialect retrieves cursor.lastrowid or an equivalent
890        after an insert() construct executes, even if the table has a
891        Sequence on it.
892        """
893        return fails_on_everything_except(
894            "mysql",
895            "mariadb",
896            "sqlite+pysqlite",
897            "sqlite+pysqlcipher",
898        )
899
900    @property
901    def dbapi_lastrowid(self):
902        """ "target backend includes a 'lastrowid' accessor on the DBAPI
903        cursor object.
904
905        """
906        return skip_if("mssql+pymssql", "crashes on pymssql") + only_on(
907            [
908                "mysql",
909                "mariadb",
910                "sqlite+pysqlite",
911                "sqlite+aiosqlite",
912                "sqlite+pysqlcipher",
913                "mssql",
914            ]
915        )
916
917    @property
918    def nullsordering(self):
919        """Target backends that support nulls ordering."""
920        return fails_on_everything_except(
921            "postgresql", "oracle", "sqlite >= 3.30.0"
922        )
923
924    @property
925    def reflects_pk_names(self):
926        """Target driver reflects the name of primary key constraints."""
927
928        return fails_on_everything_except(
929            "postgresql", "oracle", "mssql", "sqlite"
930        )
931
932    @property
933    def nested_aggregates(self):
934        """target database can select an aggregate from a subquery that's
935        also using an aggregate"""
936
937        return skip_if(["mssql", "sqlite"])
938
939    @property
940    def tuple_valued_builtin_functions(self):
941        return only_on(
942            lambda config: self._sqlite_json(config)
943            or against(config, "postgresql")
944        )
945
946    @property
947    def array_type(self):
948        return only_on(
949            [
950                lambda config: against(config, "postgresql")
951                and not against(config, "+pg8000")
952            ]
953        )
954
955    @property
956    def json_type(self):
957        return only_on(
958            [
959                lambda config: against(config, "mysql")
960                and (
961                    (
962                        not config.db.dialect._is_mariadb
963                        and against(config, "mysql >= 5.7")
964                    )
965                    or (
966                        config.db.dialect._mariadb_normalized_version_info
967                        >= (10, 2, 7)
968                    )
969                ),
970                "mariadb>=10.2.7",
971                "postgresql >= 9.3",
972                self._sqlite_json,
973                "mssql",
974            ]
975        )
976
977    @property
978    def json_index_supplementary_unicode_element(self):
979        # for sqlite see https://bugs.python.org/issue38749
980        return skip_if(
981            [
982                lambda config: against(config, "mysql")
983                and config.db.dialect._is_mariadb,
984                "mariadb",
985                "sqlite",
986            ]
987        )
988
989    @property
990    def legacy_unconditional_json_extract(self):
991        """Backend has a JSON_EXTRACT or similar function that returns a
992        valid JSON string in all cases.
993
994        Used to test a legacy feature and is not needed.
995
996        """
997        return self.json_type + only_on(
998            ["postgresql", "mysql", "mariadb", "sqlite"]
999        )
1000
1001    def _sqlite_file_db(self, config):
1002        return against(config, "sqlite") and config.db.dialect._is_url_file_db(
1003            config.db.url
1004        )
1005
1006    def _sqlite_memory_db(self, config):
1007        return against(
1008            config, "sqlite"
1009        ) and not config.db.dialect._is_url_file_db(config.db.url)
1010
1011    def _sqlite_json(self, config):
1012        if not against(config, "sqlite >= 3.9"):
1013            return False
1014        else:
1015            with config.db.connect() as conn:
1016                try:
1017                    return (
1018                        conn.exec_driver_sql(
1019                            """select json_extract('{"foo": "bar"}', """
1020                            """'$."foo"')"""
1021                        ).scalar()
1022                        == "bar"
1023                    )
1024                except exc.DBAPIError:
1025                    return False
1026
1027    @property
1028    def sqlite_memory(self):
1029        return only_on(self._sqlite_memory_db)
1030
1031    @property
1032    def reflects_json_type(self):
1033        return only_on(
1034            [
1035                lambda config: against(config, "mysql >= 5.7")
1036                and not config.db.dialect._is_mariadb,
1037                "postgresql >= 9.3",
1038                "sqlite >= 3.9",
1039            ]
1040        )
1041
1042    @property
1043    def json_array_indexes(self):
1044        return self.json_type
1045
1046    @property
1047    def datetime_literals(self):
1048        """target dialect supports rendering of a date, time, or datetime as a
1049        literal string, e.g. via the TypeEngine.literal_processor() method.
1050
1051        """
1052
1053        return fails_on_everything_except("sqlite")
1054
1055    @property
1056    def datetime(self):
1057        """target dialect supports representation of Python
1058        datetime.datetime() objects."""
1059
1060        return exclusions.open()
1061
1062    @property
1063    def datetime_microseconds(self):
1064        """target dialect supports representation of Python
1065        datetime.datetime() with microsecond objects."""
1066
1067        return skip_if(["mssql", "mysql", "mariadb", "oracle"])
1068
1069    @property
1070    def timestamp_microseconds(self):
1071        """target dialect supports representation of Python
1072        datetime.datetime() with microsecond objects but only
1073        if TIMESTAMP is used."""
1074
1075        return only_on(["oracle"])
1076
1077    @property
1078    def datetime_historic(self):
1079        """target dialect supports representation of Python
1080        datetime.datetime() objects with historic (pre 1900) values."""
1081
1082        return succeeds_if(["sqlite", "postgresql"])
1083
1084    @property
1085    def date(self):
1086        """target dialect supports representation of Python
1087        datetime.date() objects."""
1088
1089        return exclusions.open()
1090
1091    @property
1092    def date_coerces_from_datetime(self):
1093        """target dialect accepts a datetime object as the target
1094        of a date column."""
1095
1096        # does not work as of pyodbc 4.0.22
1097        return fails_on("mysql+mysqlconnector") + skip_if("mssql+pyodbc")
1098
1099    @property
1100    def date_historic(self):
1101        """target dialect supports representation of Python
1102        datetime.datetime() objects with historic (pre 1900) values."""
1103
1104        return succeeds_if(["sqlite", "postgresql"])
1105
1106    @property
1107    def time(self):
1108        """target dialect supports representation of Python
1109        datetime.time() objects."""
1110
1111        return skip_if(["oracle"])
1112
1113    @property
1114    def time_microseconds(self):
1115        """target dialect supports representation of Python
1116        datetime.time() with microsecond objects."""
1117
1118        return skip_if(["mssql", "mysql", "mariadb", "oracle"])
1119
1120    @property
1121    def precision_numerics_general(self):
1122        """target backend has general support for moderately high-precision
1123        numerics."""
1124        return exclusions.open()
1125
1126    @property
1127    def precision_numerics_enotation_small(self):
1128        """target backend supports Decimal() objects using E notation
1129        to represent very small values."""
1130        # NOTE: this exclusion isn't used in current tests.
1131        return exclusions.open()
1132
1133    @property
1134    def precision_numerics_many_significant_digits(self):
1135        """target backend supports values with many digits on both sides,
1136        such as 319438950232418390.273596, 87673.594069654243
1137
1138        """
1139
1140        def broken_cx_oracle(config):
1141            return (
1142                against(config, "oracle+cx_oracle")
1143                and config.db.dialect.cx_oracle_ver <= (6, 0, 2)
1144                and config.db.dialect.cx_oracle_ver > (6,)
1145            )
1146
1147        return fails_if(
1148            [
1149                ("sqlite", None, None, "TODO"),
1150            ]
1151        )
1152
1153    @property
1154    def cast_precision_numerics_many_significant_digits(self):
1155        """same as precision_numerics_many_significant_digits but within the
1156        context of a CAST statement (hello MySQL)
1157
1158        """
1159        return self.precision_numerics_many_significant_digits + fails_if(
1160            "mysql"
1161        )
1162
1163    @property
1164    def precision_numerics_retains_significant_digits(self):
1165        """A precision numeric type will return empty significant digits,
1166        i.e. a value such as 10.000 will come back in Decimal form with
1167        the .000 maintained."""
1168
1169        return fails_if(
1170            [
1171                ("oracle", None, None, "driver doesn't do this automatically"),
1172            ]
1173        )
1174
1175    @property
1176    def precision_generic_float_type(self):
1177        """target backend will return native floating point numbers with at
1178        least seven decimal places when using the generic Float type."""
1179
1180        return fails_if(
1181            [
1182                (
1183                    "mysql",
1184                    None,
1185                    None,
1186                    "mysql FLOAT type only returns 4 decimals",
1187                ),
1188                (
1189                    "mariadb",
1190                    None,
1191                    None,
1192                    "mysql FLOAT type only returns 4 decimals",
1193                ),
1194            ]
1195        )
1196
1197    @property
1198    def implicit_decimal_binds(self):
1199        """target backend will return a selected Decimal as a Decimal, not
1200        a string.
1201
1202        e.g.::
1203
1204            expr = decimal.Decimal("15.7563")
1205
1206            value = e.scalar(
1207                select(literal(expr))
1208            )
1209
1210            assert value == expr
1211
1212        See :ticket:`4036`
1213
1214        """
1215
1216        return exclusions.open()
1217
1218    @property
1219    def fetch_null_from_numeric(self):
1220        return skip_if(("mssql+pyodbc", None, None, "crashes due to bug #351"))
1221
1222    @property
1223    def duplicate_key_raises_integrity_error(self):
1224        return exclusions.open()
1225
1226    def _has_pg_extension(self, name):
1227        def check(config):
1228            if not against(config, "postgresql"):
1229                return False
1230            with config.db.connect() as conn:
1231                count = conn.exec_driver_sql(
1232                    "SELECT count(*) FROM pg_extension "
1233                    "WHERE extname='%s'" % name
1234                ).scalar()
1235            return bool(count)
1236
1237        return only_if(check, "needs %s extension" % name)
1238
1239    @property
1240    def hstore(self):
1241        return self._has_pg_extension("hstore")
1242
1243    @property
1244    def btree_gist(self):
1245        return self._has_pg_extension("btree_gist")
1246
1247    @property
1248    def range_types(self):
1249        def check_range_types(config):
1250            if not against(
1251                config, ["postgresql+psycopg2", "postgresql+psycopg2cffi"]
1252            ):
1253                return False
1254            try:
1255                with config.db.connect() as conn:
1256                    conn.exec_driver_sql("select '[1,2)'::int4range;").scalar()
1257                return True
1258            except Exception:
1259                return False
1260
1261        return only_if(check_range_types)
1262
1263    @property
1264    def async_dialect(self):
1265        """dialect makes use of await_() to invoke operations on the DBAPI."""
1266
1267        return only_on(
1268            LambdaPredicate(
1269                lambda config: config.db.dialect.is_async,
1270                "Async dialect required",
1271            )
1272        )
1273
1274    @property
1275    def oracle_test_dblink(self):
1276        return skip_if(
1277            lambda config: not config.file_config.has_option(
1278                "sqla_testing", "oracle_db_link"
1279            ),
1280            "oracle_db_link option not specified in config",
1281        )
1282
1283    @property
1284    def postgresql_test_dblink(self):
1285        return skip_if(
1286            lambda config: not config.file_config.has_option(
1287                "sqla_testing", "postgres_test_db_link"
1288            ),
1289            "postgres_test_db_link option not specified in config",
1290        )
1291
1292    @property
1293    def postgresql_jsonb(self):
1294        return only_on("postgresql >= 9.4") + skip_if(
1295            lambda config: config.db.dialect.driver == "pg8000"
1296            and config.db.dialect._dbapi_version <= (1, 10, 1)
1297        )
1298
1299    @property
1300    def psycopg2_native_hstore(self):
1301        return self.psycopg2_compatibility
1302
1303    @property
1304    def psycopg2_compatibility(self):
1305        return only_on(["postgresql+psycopg2", "postgresql+psycopg2cffi"])
1306
1307    @property
1308    def psycopg2_or_pg8000_compatibility(self):
1309        return only_on(
1310            [
1311                "postgresql+psycopg2",
1312                "postgresql+psycopg2cffi",
1313                "postgresql+pg8000",
1314            ]
1315        )
1316
1317    @property
1318    def percent_schema_names(self):
1319        return skip_if(
1320            ["mysql+aiomysql", "mariadb+aiomysql"],
1321            "see pr https://github.com/aio-libs/aiomysql/pull/545",
1322        )
1323
1324    @property
1325    def order_by_label_with_expression(self):
1326        return fails_if(
1327            [
1328                ("postgresql", None, None, "only simple labels allowed"),
1329                ("mssql", None, None, "only simple labels allowed"),
1330            ]
1331        )
1332
1333    def get_order_by_collation(self, config):
1334        lookup = {
1335            # will raise without quoting
1336            "postgresql": "POSIX",
1337            # note MySQL databases need to be created w/ utf8mb4 charset
1338            # for the test suite
1339            "mysql": "utf8mb4_bin",
1340            "mariadb": "utf8mb4_bin",
1341            "sqlite": "NOCASE",
1342            # will raise *with* quoting
1343            "mssql": "Latin1_General_CI_AS",
1344        }
1345        try:
1346            return lookup[config.db.name]
1347        except KeyError:
1348            raise NotImplementedError()
1349
1350    @property
1351    def skip_mysql_on_windows(self):
1352        """Catchall for a large variety of MySQL on Windows failures"""
1353
1354        return skip_if(
1355            self._has_mysql_on_windows, "Not supported on MySQL + Windows"
1356        )
1357
1358    @property
1359    def mssql_freetds(self):
1360        return only_on(["mssql+pymssql"])
1361
1362    @property
1363    def legacy_engine(self):
1364        return exclusions.skip_if(lambda config: config.db._is_future)
1365
1366    @property
1367    def ad_hoc_engines(self):
1368        return skip_if(self._sqlite_file_db)
1369
1370    @property
1371    def no_asyncio(self):
1372        def go(config):
1373            return config.db.dialect.is_async
1374
1375        return skip_if(go)
1376
1377    @property
1378    def no_mssql_freetds(self):
1379        return self.mssql_freetds.not_()
1380
1381    @property
1382    def pyodbc_fast_executemany(self):
1383        def has_fastexecutemany(config):
1384            if not against(config, "mssql+pyodbc"):
1385                return False
1386            if config.db.dialect._dbapi_version() < (4, 0, 19):
1387                return False
1388            with config.db.connect() as conn:
1389                drivername = conn.connection.connection.getinfo(
1390                    config.db.dialect.dbapi.SQL_DRIVER_NAME
1391                )
1392                # on linux this is something like 'libmsodbcsql-13.1.so.9.2'.
1393                # on Windows this is something like 'msodbcsql17.dll'.
1394                return "msodbc" in drivername
1395
1396        return only_if(
1397            has_fastexecutemany, "only on pyodbc > 4.0.19 w/ msodbc driver"
1398        )
1399
1400    @property
1401    def python_fixed_issue_8743(self):
1402        return exclusions.skip_if(
1403            lambda: sys.version_info < (2, 7, 8),
1404            "Python issue 8743 fixed in Python 2.7.8",
1405        )
1406
1407    @property
1408    def granular_timezone(self):
1409        """the datetime.timezone class, or SQLAlchemy's port, supports
1410        seconds and microseconds.
1411
1412        SQLAlchemy ported the Python 3.7 version for Python 2, so
1413        it passes on that.  For Python 3.6 and earlier, it is not supported.
1414
1415        """
1416        return exclusions.skip_if(
1417            lambda: sys.version_info >= (3,) and sys.version_info < (3, 7)
1418        )
1419
1420    @property
1421    def selectone(self):
1422        """target driver must support the literal statement 'select 1'"""
1423        return skip_if(["oracle"], "non-standard SELECT scalar syntax")
1424
1425    @property
1426    def mysql_for_update(self):
1427        return skip_if(
1428            "mysql+mysqlconnector",
1429            "lock-sensitive operations crash on mysqlconnector",
1430        )
1431
1432    @property
1433    def mysql_fsp(self):
1434        return only_if(["mysql >= 5.6.4", "mariadb"])
1435
1436    @property
1437    def mysql_fully_case_sensitive(self):
1438        return only_if(self._has_mysql_fully_case_sensitive)
1439
1440    @property
1441    def mysql_zero_date(self):
1442        def check(config):
1443            if not against(config, "mysql"):
1444                return False
1445
1446            with config.db.connect() as conn:
1447                row = conn.exec_driver_sql(
1448                    "show variables like 'sql_mode'"
1449                ).first()
1450            return not row or "NO_ZERO_DATE" not in row[1]
1451
1452        return only_if(check)
1453
1454    @property
1455    def mysql_non_strict(self):
1456        def check(config):
1457            if not against(config, "mysql"):
1458                return False
1459
1460            with config.db.connect() as conn:
1461                row = conn.exec_driver_sql(
1462                    "show variables like 'sql_mode'"
1463                ).first()
1464            return not row or "STRICT_TRANS_TABLES" not in row[1]
1465
1466        return only_if(check)
1467
1468    @property
1469    def mysql_ngram_fulltext(self):
1470        def check(config):
1471            return (
1472                against(config, "mysql")
1473                and not config.db.dialect._is_mariadb
1474                and config.db.dialect.server_version_info >= (5, 7)
1475            )
1476
1477        return only_if(check)
1478
1479    def _mysql_80(self, config):
1480        return (
1481            against(config, "mysql")
1482            and config.db.dialect._is_mysql
1483            and config.db.dialect.server_version_info >= (8,)
1484        )
1485
1486    def _mariadb_102(self, config):
1487        return (
1488            against(config, ["mysql", "mariadb"])
1489            and config.db.dialect._is_mariadb
1490            and config.db.dialect._mariadb_normalized_version_info >= (10, 2)
1491        )
1492
1493    def _mariadb_105(self, config):
1494        return (
1495            against(config, ["mysql", "mariadb"])
1496            and config.db.dialect._is_mariadb
1497            and config.db.dialect._mariadb_normalized_version_info >= (10, 5)
1498        )
1499
1500    def _mysql_and_check_constraints_exist(self, config):
1501        # 1. we have mysql / mariadb and
1502        # 2. it enforces check constraints
1503        if exclusions.against(config, ["mysql", "mariadb"]):
1504            if config.db.dialect._is_mariadb:
1505                norm_version_info = (
1506                    config.db.dialect._mariadb_normalized_version_info
1507                )
1508                return norm_version_info >= (10, 2)
1509            else:
1510                norm_version_info = config.db.dialect.server_version_info
1511                return norm_version_info >= (8, 0, 16)
1512        else:
1513            return False
1514
1515    def _mysql_check_constraints_exist(self, config):
1516        # 1. we dont have mysql / mariadb or
1517        # 2. we have mysql / mariadb that enforces check constraints
1518        return not exclusions.against(
1519            config, ["mysql", "mariadb"]
1520        ) or self._mysql_and_check_constraints_exist(config)
1521
1522    def _mysql_check_constraints_dont_exist(self, config):
1523        # 1. we have mysql / mariadb and
1524        # 2. they dont enforce check constraints
1525        return not self._mysql_check_constraints_exist(config)
1526
1527    def _mysql_not_mariadb_102(self, config):
1528        return (against(config, ["mysql", "mariadb"])) and (
1529            not config.db.dialect._is_mariadb
1530            or config.db.dialect._mariadb_normalized_version_info < (10, 2)
1531        )
1532
1533    def _mysql_not_mariadb_103(self, config):
1534        return (against(config, ["mysql", "mariadb"])) and (
1535            not config.db.dialect._is_mariadb
1536            or config.db.dialect._mariadb_normalized_version_info < (10, 3)
1537        )
1538
1539    def _mysql_not_mariadb_104(self, config):
1540        return (against(config, ["mysql", "mariadb"])) and (
1541            not config.db.dialect._is_mariadb
1542            or config.db.dialect._mariadb_normalized_version_info < (10, 4)
1543        )
1544
1545    def _has_mysql_on_windows(self, config):
1546        with config.db.connect() as conn:
1547            return (
1548                against(config, ["mysql", "mariadb"])
1549            ) and config.db.dialect._detect_casing(conn) == 1
1550
1551    def _has_mysql_fully_case_sensitive(self, config):
1552        with config.db.connect() as conn:
1553            return (
1554                against(config, "mysql")
1555                and config.db.dialect._detect_casing(conn) == 0
1556            )
1557
1558    @property
1559    def postgresql_utf8_server_encoding(self):
1560        def go(config):
1561            if not against(config, "postgresql"):
1562                return False
1563
1564            with config.db.connect() as conn:
1565                enc = conn.exec_driver_sql("show server_encoding").scalar()
1566                return enc.lower() == "utf8"
1567
1568        return only_if(go)
1569
1570    @property
1571    def cxoracle6_or_greater(self):
1572        return only_if(
1573            lambda config: against(config, "oracle+cx_oracle")
1574            and config.db.dialect.cx_oracle_ver >= (6,)
1575        )
1576
1577    @property
1578    def oracle5x(self):
1579        return only_if(
1580            lambda config: against(config, "oracle+cx_oracle")
1581            and config.db.dialect.cx_oracle_ver < (6,)
1582        )
1583
1584    @property
1585    def computed_columns(self):
1586        return skip_if(["postgresql < 12", "sqlite < 3.31", "mysql < 5.7"])
1587
1588    @property
1589    def python_profiling_backend(self):
1590        return only_on([self._sqlite_memory_db])
1591
1592    @property
1593    def computed_columns_stored(self):
1594        return self.computed_columns + skip_if(["oracle"])
1595
1596    @property
1597    def computed_columns_virtual(self):
1598        return self.computed_columns + skip_if(["postgresql"])
1599
1600    @property
1601    def computed_columns_default_persisted(self):
1602        return self.computed_columns + only_if("postgresql")
1603
1604    @property
1605    def computed_columns_reflect_persisted(self):
1606        return self.computed_columns + skip_if("oracle")
1607
1608    @property
1609    def regexp_match(self):
1610        return only_on(["postgresql", "mysql", "mariadb", "oracle", "sqlite"])
1611
1612    @property
1613    def regexp_replace(self):
1614        return only_on(["postgresql", "mysql>=8", "mariadb", "oracle"])
1615
1616    @property
1617    def supports_distinct_on(self):
1618        """If a backend supports the DISTINCT ON in a select"""
1619        return only_if(["postgresql"])
1620
1621    @property
1622    def supports_for_update_of(self):
1623        return only_if(lambda config: config.db.dialect.supports_for_update_of)
1624
1625    @property
1626    def sequences_in_other_clauses(self):
1627        """sequences allowed in WHERE, GROUP BY, HAVING, etc."""
1628        return skip_if(["mssql", "oracle"])
1629
1630    @property
1631    def supports_lastrowid_for_expressions(self):
1632        """cursor.lastrowid works if an explicit SQL expression was used."""
1633        return only_on(["sqlite", "mysql", "mariadb"])
1634
1635    @property
1636    def supports_sequence_for_autoincrement_column(self):
1637        """for mssql, autoincrement means IDENTITY, not sequence"""
1638        return skip_if("mssql")
1639
1640    @property
1641    def identity_columns(self):
1642        return only_if(["postgresql >= 10", "oracle >= 12", "mssql"])
1643
1644    @property
1645    def identity_columns_standard(self):
1646        return self.identity_columns + skip_if("mssql")
1647
1648    @property
1649    def index_reflects_included_columns(self):
1650        return only_on(["postgresql >= 11", "mssql"])
1651
1652    # mssql>= 11 -> >= MS_2012_VERSION
1653
1654    @property
1655    def fetch_first(self):
1656        return only_on(
1657            ["postgresql", "mssql >= 11", "oracle >= 12", "mariadb >= 10.6"]
1658        )
1659
1660    @property
1661    def fetch_percent(self):
1662        return only_on(["mssql >= 11", "oracle >= 12"])
1663
1664    @property
1665    def fetch_ties(self):
1666        return only_on(
1667            [
1668                "postgresql >= 13",
1669                "mssql >= 11",
1670                "oracle >= 12",
1671                "mariadb >= 10.6",
1672            ]
1673        )
1674
1675    @property
1676    def fetch_no_order_by(self):
1677        return only_on(["postgresql", "oracle >= 12", "mariadb >= 10.6"])
1678
1679    @property
1680    def fetch_offset_with_options(self):
1681        # use together with fetch_first
1682        return skip_if("mssql")
1683
1684    @property
1685    def fetch_expression(self):
1686        # use together with fetch_first
1687        return skip_if("mariadb")
1688
1689    @property
1690    def autoincrement_without_sequence(self):
1691        return skip_if("oracle")
1692
1693    @property
1694    def reflect_tables_no_columns(self):
1695        # so far sqlite, mariadb, mysql don't support this
1696        return only_on(["postgresql"])
1697