Repository: dbeatty10/dbt-mysql (language: python)

Issue:
Test with current versions of MariaDB



Discussion / hints:
## Attempt
I did a quick tests of MariaDB 10.5 (with both dbt 0.19.0rc1 and 0.18.1) using the mysql-connector-python driver.

## Result
Neither worked out-of-the-box.

## Next steps
The best move might be to give MariaDB a stand-alone GitHub repository.

An alternative would be keep it within the `dbt-mysql` package, but create a separate `mariadb` adapter.

Either way, we'll probably need to install the [mariadb](https://mariadb.com/resources/blog/how-to-connect-python-programs-to-mariadb/) package from PyPi.




[This MariaDB bug](https://jira.mariadb.org/browse/MDEV-22781) is a bit of a show-stopper. It will affect any ephemeral models used within a view.

"A fix for this bug was pushed into 10.2", but I'm not sure if this fix has made it into any public releases by MariaDB yet.
From 2021-02-22: [MariaDB 10.5.9, 10.4.18, 10.3.28 and 10.2.37 now available](https://mariadb.org/mariadb-10-5-9-10-4-18-10-3-28-and-10-2-37-now-available/).

This should fix [this bug](https://jira.mariadb.org/browse/MDEV-22781):
![image](https://user-images.githubusercontent.com/44704949/109012221-00b15080-766f-11eb-8c89-3bca1abd36e7.png)

Hi @dbeatty10!

Sorry 🤷‍♂️ for troubling you.
If you know, can you share your plans about MariaDB? It is going in the dbt-mariadb direction or integrating in dbt-mysql?

Best regards

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68e15d5..37b7ce8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## Unreleased (TBD)
 - Integration tests for MySQL 5.7 ([#70](https://github.com/dbeatty10/dbt-mysql/issues/70), [#71](https://github.com/dbeatty10/dbt-mysql/pull/71))
 - Integration tests for MariaDB 10.5 ([#72](https://github.com/dbeatty10/dbt-mysql/pull/72))
+- Support for MariaDB 10.5 ([#32](https://github.com/dbeatty10/dbt-mysql/issues/32), [#73](https://github.com/dbeatty10/dbt-mysql/pull/73))
 
 ## dbt-mysql 0.19.0.1 (February 19, 2022)
 - Optional `ssl_disabled` property ([#67](https://github.com/dbeatty10/dbt-mysql/pull/67))
diff --git a/README.md b/README.md
index df73d1b..8ed757f 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,15 @@
 # dbt-mysql
 
-This plugin ports [dbt](https://getdbt.com) functionality to MySQL.
+This plugin ports [dbt](https://getdbt.com) functionality to MySQL and MariaDB.
 
 This is an experimental plugin:
 - We have not tested it extensively
 - Storage engines other than the default of InnoDB are untested
-- MariaDB compatibility is untested
-- Only tested with [dbt-adapter-tests](https://github.com/fishtown-analytics/dbt-adapter-tests) with MySQL 5.6, 5.7, and 8.0
+- Only tested with [dbt-adapter-tests](https://github.com/fishtown-analytics/dbt-adapter-tests) with the following:
+  - MySQL 5.6
+  - MySQL 5.7
+  - MySQL 8.0
+  - MariaDB 10.5
 - Compatiblity with other [dbt packages](https://hub.getdbt.com/) (like [dbt_utils](https://hub.getdbt.com/fishtown-analytics/dbt_utils/latest/)) is also untested
 
 Please read these docs carefully and use at your own risk. [Issues](https://github.com/dbeatty10/dbt-mysql/issues/new) and [PRs](https://github.com/dbeatty10/dbt-mysql/blob/main/CONTRIBUTING.rst#contributing) welcome!
@@ -34,17 +37,17 @@ $ pip install dbt-mysql
 
 ### Supported features
 
-| 5.6 / 5.7 | 8.0 | Feature                     |
-|:---------:|:---:|-----------------------------|
-|     ✅     |  ✅  | Table materialization       |
-|     ✅     |  ✅  | View materialization        |
-|     ✅     |  ✅  | Incremental materialization |
-|     ❌     |  ✅  | Ephemeral materialization   |
-|     ✅     |  ✅  | Seeds                       |
-|     ✅     |  ✅  | Sources                     |
-|     ✅     |  ✅  | Custom data tests           |
-|     ✅     |  ✅  | Docs generate               |
-|     🤷     |  ✅  | Snapshots                   |
+| MariaDB 10.5 | MySQL 5.6 / 5.7 | MySQL 8.0 | Feature                     |
+|:---------:|:---------:|:---:|-----------------------------|
+|     ✅     |     ✅     |  ✅  | Table materialization       |
+|     ✅     |     ✅     |  ✅  | View materialization        |
+|     ✅     |     ✅     |  ✅  | Incremental materialization |
+|     ✅     |     ❌     |  ✅  | Ephemeral materialization   |
+|     ✅     |     ✅     |  ✅  | Seeds                       |
+|     ✅     |     ✅     |  ✅  | Sources                     |
+|     ✅     |     ✅     |  ✅  | Custom data tests           |
+|     ✅     |     ✅     |  ✅  | Docs generate               |
+|     🤷     |     🤷     |  ✅  | Snapshots                   |
 
 Notes:
 - Ephemeral materializations rely upon [Common Table Expressions](https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL) (CTEs), which are not supported until MySQL 8.0
@@ -57,7 +60,6 @@ dbt snapshots might not work properly due to [automatic initialization and updat
 - the output of `SHOW GLOBAL VARIABLES LIKE 'explicit_defaults_for_timestamp'` has a value of `OFF`
 
 A solution is to include the following in a `*.cnf` file:
-Configuration to include in a `*.cnf` file:
 ```
 [mysqld]
 explicit_defaults_for_timestamp = true
@@ -66,11 +68,9 @@ explicit_defaults_for_timestamp = true
 ##### MySQL 5.7 configuration gotchas
 
 dbt snapshots might not work properly due to [automatic initialization and updating for `TIMESTAMP`](https://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html) if:
-dbt snapshots might not work properly if:
 - the output of `SHOW VARIABLES LIKE 'sql_mode'` includes `NO_ZERO_DATE`
 
 A solution is to include the following in a `*.cnf` file:
-Configuration to include in a `*.cnf` file:
 ```
 [mysqld]
 explicit_defaults_for_timestamp = true
@@ -80,9 +80,9 @@ where `{other_sql_modes}` is the rest of the modes from the `SHOW VARIABLES LIKE
 
 ### Configuring your profile
 
-A dbt profile can be configured to run against MySQL using the following configuration example:
+A dbt profile can be configured to run against MySQL using configuration example below.
 
-Use `type: mysql` for MySQL 8.x and `type: mysql5` for MySQL 5.x
+Use `type: mysql` for MySQL 8.x, `type: mysql5` for MySQL 5.x, and `type: mariadb` for MariaDB.
 
 **Example entry for profiles.yml:**
 
@@ -102,7 +102,7 @@ your_profile_name:
 
 | Option          | Description                                                                         | Required?                                                          | Example                                        |
 | --------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------- |
-| type            | The specific adapter to use                                                         | Required                                                           | `mysql` or `mysql5`                            |
+| type            | The specific adapter to use                                                         | Required                                                           | `mysql`, `mysql5` or `mariadb`                            |
 | server          | The server (hostname) to connect to                                                 | Required                                                           | `yourorg.mysqlhost.com`                        |
 | port            | The port to use                                                                     | Optional                                                           | `3306`                                         |
 | schema          | Specify the schema (database) to build models into                                  | Required                                                           | `analytics`                                    |
diff --git a/dbt/adapters/mariadb/__init__.py b/dbt/adapters/mariadb/__init__.py
new file mode 100644
index 0000000..3896fd8
--- /dev/null
+++ b/dbt/adapters/mariadb/__init__.py
@@ -0,0 +1,14 @@
+from dbt.adapters.mariadb.connections import MariaDBConnectionManager  # noqa
+from dbt.adapters.mariadb.connections import MariaDBCredentials
+from dbt.adapters.mariadb.relation import MariaDBRelation  # noqa
+from dbt.adapters.mariadb.column import MariaDBColumn  # noqa
+from dbt.adapters.mariadb.impl import MariaDBAdapter
+
+from dbt.adapters.base import AdapterPlugin
+from dbt.include import mariadb
+
+
+Plugin = AdapterPlugin(
+    adapter=MariaDBAdapter,
+    credentials=MariaDBCredentials,
+    include_path=mariadb.PACKAGE_PATH)
diff --git a/dbt/adapters/mariadb/__version__.py b/dbt/adapters/mariadb/__version__.py
new file mode 100644
index 0000000..7efe7ff
--- /dev/null
+++ b/dbt/adapters/mariadb/__version__.py
@@ -0,0 +1,1 @@
+version = "0.19.0.1"
diff --git a/dbt/adapters/mariadb/column.py b/dbt/adapters/mariadb/column.py
new file mode 100644
index 0000000..a6fe515
--- /dev/null
+++ b/dbt/adapters/mariadb/column.py
@@ -0,0 +1,24 @@
+from dataclasses import dataclass
+from typing import TypeVar, Optional, Dict, Any
+
+from dbt.adapters.base.column import Column
+
+Self = TypeVar('Self', bound='MariaDBColumn')
+
+
+@dataclass
+class MariaDBColumn(Column):
+    table_database: Optional[str] = None
+    table_schema: Optional[str] = None
+    table_name: Optional[str] = None
+    table_type: Optional[str] = None
+    table_owner: Optional[str] = None
+    table_stats: Optional[Dict[str, Any]] = None
+    column_index: Optional[int] = None
+
+    @property
+    def quoted(self) -> str:
+        return '`{}`'.format(self.column)
+
+    def __repr__(self) -> str:
+        return "<MariaDBColumn {} ({})>".format(self.name, self.data_type)
diff --git a/dbt/adapters/mariadb/connections.py b/dbt/adapters/mariadb/connections.py
new file mode 100644
index 0000000..e51a769
--- /dev/null
+++ b/dbt/adapters/mariadb/connections.py
@@ -0,0 +1,162 @@
+from contextlib import contextmanager
+
+import mysql.connector
+
+import dbt.exceptions
+from dbt.adapters.sql import SQLConnectionManager
+from dbt.contracts.connection import AdapterResponse
+from dbt.contracts.connection import Connection
+from dbt.contracts.connection import Credentials
+from dbt.logger import GLOBAL_LOGGER as logger
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class MariaDBCredentials(Credentials):
+    server: str
+    port: Optional[int]
+    database: Optional[str]
+    schema: str
+    username: Optional[str]
+    password: Optional[str]
+    charset: Optional[str]
+    ssl_disabled: Optional[bool]
+
+    _ALIASES = {
+        "UID": "username",
+        "user": "username",
+        "PWD": "password",
+        "host": "server",
+    }
+
+    def __post_init__(self):
+        # Database and schema are treated as the same thing
+        if (
+            self.database is not None and
+            self.database != self.schema
+        ):
+            raise dbt.exceptions.RuntimeException(
+                f"    schema: {self.schema} \n"
+                f"    database: {self.database} \n"
+                f"On MariaDB, database must be omitted or have the same value as"
+                f" schema."
+            )
+        self.database = None
+
+    @property
+    def type(self):
+        return "mariadb"
+
+    def _connection_keys(self):
+        """
+        Returns an iterator of keys to pretty-print in 'dbt debug'
+        """
+        return (
+            "server",
+            "port",
+            "database",
+            "schema",
+            "user",
+        )
+
+
+class MariaDBConnectionManager(SQLConnectionManager):
+    TYPE = "mariadb"
+
+    @classmethod
+    def open(cls, connection):
+        if connection.state == 'open':
+            logger.debug('Connection is already open, skipping open.')
+            return connection
+
+        credentials = cls.get_credentials(connection.credentials)
+        kwargs = {}
+
+        kwargs["host"] = credentials.server
+        kwargs["user"] = credentials.username
+        kwargs["passwd"] = credentials.password
+
+        if credentials.ssl_disabled:
+            kwargs["ssl_disabled"] = credentials.ssl_disabled
+
+        if credentials.port:
+            kwargs["port"] = credentials.port
+
+        try:
+            connection.handle = mysql.connector.connect(**kwargs)
+            connection.state = 'open'
+        except mysql.connector.Error:
+
+            try:
+                logger.debug("Failed connection without supplying the `database`. "
+                             "Trying again with `database` included.")
+
+                # Try again with the database included
+                kwargs["database"] = credentials.schema
+
+                connection.handle = mysql.connector.connect(**kwargs)
+                connection.state = 'open'
+            except mysql.connector.Error as e:
+
+                logger.debug("Got an error when attempting to open a MariaDB "
+                             "connection: '{}'"
+                             .format(e))
+
+                connection.handle = None
+                connection.state = 'fail'
+
+                raise dbt.exceptions.FailedToConnectException(str(e))
+
+        return connection
+
+    @classmethod
+    def get_credentials(cls, credentials):
+        return credentials
+
+    def cancel(self, connection: Connection):
+        connection.handle.close()
+
+    @contextmanager
+    def exception_handler(self, sql):
+        try:
+            yield
+
+        except mysql.connector.DatabaseError as e:
+            logger.debug('MariaDB error: {}'.format(str(e)))
+
+            try:
+                self.rollback_if_open()
+            except mysql.connector.Error:
+                logger.debug("Failed to release connection!")
+                pass
+
+            raise dbt.exceptions.DatabaseException(str(e).strip()) from e
+
+        except Exception as e:
+            logger.debug("Error running SQL: {}", sql)
+            logger.debug("Rolling back transaction.")
+            self.rollback_if_open()
+            if isinstance(e, dbt.exceptions.RuntimeException):
+                # during a sql query, an internal to dbt exception was raised.
+                # this sounds a lot like a signal handler and probably has
+                # useful information, so raise it without modification.
+                raise
+
+            raise dbt.exceptions.RuntimeException(e) from e
+
+    @classmethod
+    def get_status(cls, cursor):
+        # There's no real way to get this from mysql-connector-python.
+        # So just return "OK".
+        return "OK"
+
+    @classmethod
+    def get_response(cls, cursor) -> AdapterResponse:
+        code = "Unknown cursor state/status"
+
+        return AdapterResponse(
+            _message="{} {}".format(code, cursor.rowcount),
+            rows_affected=cursor.rowcount,
+            code=code
+        )
diff --git a/dbt/adapters/mariadb/impl.py b/dbt/adapters/mariadb/impl.py
new file mode 100644
index 0000000..c2f8730
--- /dev/null
+++ b/dbt/adapters/mariadb/impl.py
@@ -0,0 +1,272 @@
+from concurrent.futures import Future
+from typing import Optional, List, Dict, Any, Iterable
+import agate
+
+import dbt
+import dbt.exceptions
+
+from dbt.adapters.base.impl import catch_as_completed
+from dbt.adapters.sql import SQLAdapter
+from dbt.adapters.mariadb import MariaDBConnectionManager
+from dbt.adapters.mariadb import MariaDBRelation
+from dbt.adapters.mariadb import MariaDBColumn
+from dbt.adapters.base import BaseRelation
+from dbt.clients.agate_helper import DEFAULT_TYPE_TESTER
+from dbt.logger import GLOBAL_LOGGER as logger
+from dbt.utils import executor
+
+LIST_SCHEMAS_MACRO_NAME = 'list_schemas'
+LIST_RELATIONS_MACRO_NAME = 'list_relations_without_caching'
+
+
+class MariaDBAdapter(SQLAdapter):
+    Relation = MariaDBRelation
+    Column = MariaDBColumn
+    ConnectionManager = MariaDBConnectionManager
+
+    @classmethod
+    def date_function(cls):
+        return 'current_date()'
+
+    @classmethod
+    def convert_datetime_type(
+            cls, agate_table: agate.Table, col_idx: int
+    ) -> str:
+        return "timestamp"
+
+    def quote(self, identifier):
+        return '`{}`'.format(identifier)
+
+    def list_relations_without_caching(
+        self, schema_relation: MariaDBRelation
+    ) -> List[MariaDBRelation]:
+        kwargs = {'schema_relation': schema_relation}
+        try:
+            results = self.execute_macro(
+                LIST_RELATIONS_MACRO_NAME,
+                kwargs=kwargs
+            )
+        except dbt.exceptions.RuntimeException as e:
+            errmsg = getattr(e, 'msg', '')
+            if f"MariaDB database '{schema_relation}' not found" in errmsg:
+                return []
+            else:
+                description = "Error while retrieving information about"
+                logger.debug(f"{description} {schema_relation}: {e.msg}")
+                return []
+
+        relations = []
+        for row in results:
+            if len(row) != 4:
+                raise dbt.exceptions.RuntimeException(
+                    "Invalid value from "
+                    f'"mariadb__list_relations_without_caching({kwargs})", '
+                    f'got {len(row)} values, expected 4'
+                )
+            _, name, _schema, relation_type = row
+            relation = self.Relation.create(
+                schema=_schema,
+                identifier=name,
+                type=relation_type
+            )
+            relations.append(relation)
+
+        return relations
+
+    def get_columns_in_relation(self, relation: Relation) -> List[MariaDBColumn]:
+        rows: List[agate.Row] = super().get_columns_in_relation(relation)
+        return self.parse_show_columns(relation, rows)
+
+    def _get_columns_for_catalog(
+        self, relation: MariaDBRelation
+    ) -> Iterable[Dict[str, Any]]:
+        columns = self.get_columns_in_relation(relation)
+
+        for column in columns:
+            # convert MariaDBColumns into catalog dicts
+            as_dict = column.to_dict()
+            as_dict['column_name'] = as_dict.pop('column', None)
+            as_dict['column_type'] = as_dict.pop('dtype')
+            as_dict['table_database'] = None
+            yield as_dict
+
+    def get_relation(
+        self, database: str, schema: str, identifier: str
+    ) -> Optional[BaseRelation]:
+        if not self.Relation.include_policy.database:
+            database = None
+
+        return super().get_relation(database, schema, identifier)
+
+    def parse_show_columns(
+            self,
+            relation: Relation,
+            raw_rows: List[agate.Row]
+    ) -> List[MariaDBColumn]:
+        return [MariaDBColumn(
+            table_database=None,
+            table_schema=relation.schema,
+            table_name=relation.name,
+            table_type=relation.type,
+            table_owner=None,
+            table_stats=None,
+            column=column.column,
+            column_index=idx,
+            dtype=column.dtype,
+        ) for idx, column in enumerate(raw_rows)]
+
+    def get_catalog(self, manifest):
+        schema_map = self._get_catalog_schemas(manifest)
+        if len(schema_map) > 1:
+            dbt.exceptions.raise_compiler_error(
+                f'Expected only one database in get_catalog, found '
+                f'{list(schema_map)}'
+            )
+
+        with executor(self.config) as tpe:
+            futures: List[Future[agate.Table]] = []
+            for info, schemas in schema_map.items():
+                for schema in schemas:
+                    futures.append(tpe.submit_connected(
+                        self, schema,
+                        self._get_one_catalog, info, [schema], manifest
+                    ))
+            catalogs, exceptions = catch_as_completed(futures)
+        return catalogs, exceptions
+
+    def _get_one_catalog(
+        self, information_schema, schemas, manifest,
+    ) -> agate.Table:
+        if len(schemas) != 1:
+            dbt.exceptions.raise_compiler_error(
+                f'Expected only one schema in mariadb _get_one_catalog, found '
+                f'{schemas}'
+            )
+
+        database = information_schema.database
+        schema = list(schemas)[0]
+
+        columns: List[Dict[str, Any]] = []
+        for relation in self.list_relations(database, schema):
+            logger.debug("Getting table schema for relation {}", relation)
+            columns.extend(self._get_columns_for_catalog(relation))
+        return agate.Table.from_object(
+            columns, column_types=DEFAULT_TYPE_TESTER
+        )
+
+    def check_schema_exists(self, database, schema):
+        results = self.execute_macro(
+            LIST_SCHEMAS_MACRO_NAME,
+            kwargs={'database': database}
+        )
+
+        exists = True if schema in [row[0] for row in results] else False
+        return exists
+
+    # Methods used in adapter tests
+    def update_column_sql(
+        self,
+        dst_name: str,
+        dst_column: str,
+        clause: str,
+        where_clause: Optional[str] = None,
+    ) -> str:
+        clause = f'update {dst_name} set {dst_column} = {clause}'
+        if where_clause is not None:
+            clause += f' where {where_clause}'
+        return clause
+
+    def timestamp_add_sql(
+        self, add_to: str, number: int = 1, interval: str = 'hour'
+    ) -> str:
+        # for backwards compatibility, we're compelled to set some sort of
+        # default. A lot of searching has lead me to believe that the
+        # '+ interval' syntax used in postgres/redshift is relatively common
+        # and might even be the SQL standard's intention.
+        return f"date_add({add_to}, interval {number} {interval})"
+
+    def string_add_sql(
+        self, add_to: str, value: str, location='append',
+    ) -> str:
+        if location == 'append':
+            return f"concat({add_to}, '{value}')"
+        elif location == 'prepend':
+            return f"concat({value}, '{add_to}')"
+        else:
+            raise dbt.exceptions.RuntimeException(
+                f'Got an unexpected location value of "{location}"'
+            )
+
+    def get_rows_different_sql(
+        self,
+        relation_a: MariaDBRelation,
+        relation_b: MariaDBRelation,
+        column_names: Optional[List[str]] = None,
+    ) -> str:
+        # This method only really exists for test reasons
+        names: List[str]
+        if column_names is None:
+            columns = self.get_columns_in_relation(relation_a)
+            names = sorted((self.quote(c.name) for c in columns))
+        else:
+            names = sorted((self.quote(n) for n in column_names))
+
+        alias_a = "A"
+        alias_b = "B"
+        columns_csv_a = ', '.join([f"{alias_a}.{name}" for name in names])
+        columns_csv_b = ', '.join([f"{alias_b}.{name}" for name in names])
+        join_condition = " AND ".join(
+            [f"{alias_a}.{name} = {alias_b}.{name}" for name in names]
+        )
+        first_column = names[0]
+
+        # There is no EXCEPT or MINUS operator, so we need to simulate it
+        COLUMNS_EQUAL_SQL = '''
+        SELECT
+            row_count_diff.difference as row_count_difference,
+            diff_count.num_missing as num_mismatched
+        FROM (
+            SELECT
+                1 as id,
+                table_a.num_rows - table_b.num_rows as difference
+            FROM
+                (SELECT COUNT(*) as num_rows FROM {relation_a}) as table_a,
+                (SELECT COUNT(*) as num_rows FROM {relation_b}) as table_b
+            ) as row_count_diff
+        INNER JOIN (
+            SELECT
+                1 as id,
+                COUNT(*) as num_missing FROM (
+
+                    SELECT
+                        {columns_a}
+                    FROM {relation_a} as {alias_a}
+                    LEFT OUTER JOIN {relation_b} as {alias_b}
+                        ON {join_condition}
+                    WHERE {alias_b}.{first_column} is null
+
+                    UNION ALL
+
+                    SELECT
+                        {columns_b}
+                    FROM {relation_b} as {alias_b}
+                    LEFT OUTER JOIN {relation_a} as {alias_a}
+                        ON {join_condition}
+                    WHERE {alias_a}.{first_column} is null
+
+                ) as missing
+            ) as diff_count ON row_count_diff.id = diff_count.id
+        '''.strip()
+
+        sql = COLUMNS_EQUAL_SQL.format(
+            alias_a=alias_a,
+            alias_b=alias_b,
+            first_column=first_column,
+            columns_a=columns_csv_a,
+            columns_b=columns_csv_b,
+            join_condition=join_condition,
+            relation_a=str(relation_a),
+            relation_b=str(relation_b),
+        )
+
+        return sql
diff --git a/dbt/adapters/mariadb/relation.py b/dbt/adapters/mariadb/relation.py
new file mode 100644
index 0000000..c0ee3ca
--- /dev/null
+++ b/dbt/adapters/mariadb/relation.py
@@ -0,0 +1,37 @@
+from dataclasses import dataclass
+
+from dbt.adapters.base.relation import BaseRelation, Policy
+from dbt.exceptions import RuntimeException
+
+
+@dataclass
+class MariaDBQuotePolicy(Policy):
+    database: bool = False
+    schema: bool = True
+    identifier: bool = True
+
+
+@dataclass
+class MariaDBIncludePolicy(Policy):
+    database: bool = False
+    schema: bool = True
+    identifier: bool = True
+
+
+@dataclass(frozen=True, eq=False, repr=False)
+class MariaDBRelation(BaseRelation):
+    quote_policy: MariaDBQuotePolicy = MariaDBQuotePolicy()
+    include_policy: MariaDBIncludePolicy = MariaDBIncludePolicy()
+    quote_character: str = '`'
+
+    def __post_init__(self):
+        if self.database != self.schema and self.database:
+            raise RuntimeException(f'Cannot set database {self.database} in MariaDB!')
+
+    def render(self):
+        if self.include_policy.database and self.include_policy.schema:
+            raise RuntimeException(
+                "Got a MariaDB relation with schema and database set to "
+                "include, but only one can be set"
+            )
+        return super().render()
diff --git a/dbt/include/mariadb/__init__.py b/dbt/include/mariadb/__init__.py
new file mode 100644
index 0000000..564a3d1
--- /dev/null
+++ b/dbt/include/mariadb/__init__.py
@@ -0,0 +1,2 @@
+import os
+PACKAGE_PATH = os.path.dirname(__file__)
diff --git a/dbt/include/mariadb/dbt_project.yml b/dbt/include/mariadb/dbt_project.yml
new file mode 100644
index 0000000..8067aa8
--- /dev/null
+++ b/dbt/include/mariadb/dbt_project.yml
@@ -0,0 +1,6 @@
+
+name: dbt_mariadb
+version: 1.0
+config-version: 2
+
+macro-paths: ["macros"]
diff --git a/dbt/include/mariadb/macros/adapters.sql b/dbt/include/mariadb/macros/adapters.sql
new file mode 100644
index 0000000..51f7476
--- /dev/null
+++ b/dbt/include/mariadb/macros/adapters.sql
@@ -0,0 +1,103 @@
+
+{% macro mariadb__list_schemas(database) %}
+    {% call statement('list_schemas', fetch_result=True, auto_begin=False) -%}
+        select distinct schema_name
+        from information_schema.schemata
+    {%- endcall %}
+
+    {{ return(load_result('list_schemas').table) }}
+{% endmacro %}
+
+{% macro mariadb__create_schema(relation) -%}
+  {%- call statement('create_schema') -%}
+    create schema if not exists {{ relation.without_identifier() }}
+  {%- endcall -%}
+{% endmacro %}
+
+{% macro mariadb__drop_schema(relation) -%}
+  {%- call statement('drop_schema') -%}
+    drop schema if exists {{ relation.without_identifier() }}
+  {% endcall %}
+{% endmacro %}
+
+{% macro mariadb__drop_relation(relation) -%}
+    {% call statement('drop_relation', auto_begin=False) -%}
+        drop {{ relation.type }} if exists {{ relation }}
+    {%- endcall %}
+{% endmacro %}
+
+{% macro mariadb__truncate_relation(relation) -%}
+    {% call statement('truncate_relation') -%}
+      truncate table {{ relation }}
+    {%- endcall %}
+{% endmacro %}
+
+{% macro mariadb__create_table_as(temporary, relation, sql) -%}
+  {%- set sql_header = config.get('sql_header', none) -%}
+
+  {{ sql_header if sql_header is not none }}
+
+  create {% if temporary: -%}temporary{%- endif %} table
+    {{ relation.include(database=False) }}
+    {{ sql }}
+{% endmacro %}
+
+{% macro mariadb__create_view_as(relation, sql) -%}
+  {%- set sql_header = config.get('sql_header', none) -%}
+
+  {{ sql_header if sql_header is not none }}
+  create view {{ relation }} as
+    {{ sql }}
+{%- endmacro %}
+
+{% macro mariadb__current_timestamp() -%}
+  current_timestamp()
+{%- endmacro %}
+
+{% macro mariadb__rename_relation(from_relation, to_relation) -%}
+  {#
+    Rename fails when the relation already exists, so a 2-step process is needed:
+    1. Drop the existing relation
+    2. Rename the new relation to existing relation
+  #}
+  {% call statement('drop_relation') %}
+    drop {{ to_relation.type }} if exists {{ to_relation }} cascade
+  {% endcall %}
+  {% call statement('rename_relation') %}
+    rename table {{ from_relation }} to {{ to_relation }}
+  {% endcall %}
+{% endmacro %}
+
+{% macro mariadb__check_schema_exists(database, schema) -%}
+    {# no-op #}
+    {# see MariaDBAdapter.check_schema_exists() #}
+{% endmacro %}
+
+{% macro mariadb__get_columns_in_relation(relation) -%}
+    {% call statement('get_columns_in_relation', fetch_result=True) %}
+        show columns from {{ relation.schema }}.{{ relation.identifier }}
+    {% endcall %}
+
+    {% set table = load_result('get_columns_in_relation').table %}
+    {{ return(sql_convert_columns_in_relation(table)) }}
+{% endmacro %}
+
+{% macro mariadb__list_relations_without_caching(schema_relation) %}
+  {% call statement('list_relations_without_caching', fetch_result=True) -%}
+    select
+      null as "database",
+      table_name as name,
+      table_schema as "schema",
+      case when table_type = 'BASE TABLE' then 'table'
+           when table_type = 'VIEW' then 'view'
+           else table_type
+      end as table_type
+    from information_schema.tables
+    where table_schema = '{{ schema_relation.schema }}'
+  {% endcall %}
+  {{ return(load_result('list_relations_without_caching').table) }}
+{% endmacro %}
+
+{% macro mariadb__generate_database_name(custom_database_name=none, node=none) -%}
+  {% do return(None) %}
+{%- endmacro %}
diff --git a/dbt/include/mariadb/macros/catalog.sql b/dbt/include/mariadb/macros/catalog.sql
new file mode 100644
index 0000000..395b7c0
--- /dev/null
+++ b/dbt/include/mariadb/macros/catalog.sql
@@ -0,0 +1,57 @@
+
+{% macro mariadb__get_catalog(information_schema, schemas) -%}
+    {%- call statement('catalog', fetch_result=True) -%}
+    select
+        columns.table_database,
+        columns.table_schema,
+        columns.table_name,
+        tables.table_type,
+        columns.table_comment,
+        tables.table_owner,
+        columns.column_name,
+        columns.column_index,
+        columns.column_type,
+        columns.column_comment
+    from
+    (
+        select
+            null as "table_database",
+            table_schema as "table_schema",
+            table_name as "table_name",
+            case when table_type = 'BASE TABLE' then 'table'
+                 when table_type = 'VIEW' then 'view'
+                 else table_type
+            end as "table_type",
+            null as "table_owner"
+
+        from information_schema.tables
+    )
+    as tables
+    join
+    (
+        select
+            null as "table_database",
+            table_schema as "table_schema",
+            table_name as "table_name",
+            null as "table_comment",
+
+            column_name as "column_name",
+            ordinal_position as "column_index",
+            data_type as "column_type",
+            null as "column_comment"
+
+        from information_schema.columns
+    )
+    as columns using (table_schema, table_name)
+    where table_schema not in ('information_schema', 'performance_schema', 'mysql', 'sys')
+    and (
+    {%- for schema in schemas -%}
+      upper(table_schema) = upper('{{ schema }}'){%- if not loop.last %} or {% endif -%}
+    {%- endfor -%}
+    )
+    order by column_index
+    {%- endcall -%}
+
+    {{ return(load_result('catalog').table) }}
+
+{%- endmacro %}
diff --git a/dbt/include/mariadb/macros/materializations/incremental/helpers.sql b/dbt/include/mariadb/macros/materializations/incremental/helpers.sql
new file mode 100644
index 0000000..baa999f
--- /dev/null
+++ b/dbt/include/mariadb/macros/materializations/incremental/helpers.sql
@@ -0,0 +1,23 @@
+
+{% macro incremental_delete(tmp_relation, target_relation, unique_key=none, statement_name="pre_main") %}
+    {%- if unique_key is not none -%}
+    delete
+    from {{ target_relation }}
+    where ({{ unique_key }}) in (
+        select ({{ unique_key }})
+        from {{ tmp_relation }}
+    )
+    {%- endif %}
+
+{%- endmacro %}
+
+{% macro incremental_insert(tmp_relation, target_relation, unique_key=none, statement_name="main") %}
+    {%- set dest_columns = adapter.get_columns_in_relation(target_relation) -%}
+    {%- set dest_cols_csv = dest_columns | map(attribute='quoted') | join(', ') -%}
+
+    insert into {{ target_relation }} ({{ dest_cols_csv }})
+    (
+       select {{ dest_cols_csv }}
+       from {{ tmp_relation }}
+    )
+{%- endmacro %}
diff --git a/dbt/include/mariadb/macros/materializations/incremental/incremental.sql b/dbt/include/mariadb/macros/materializations/incremental/incremental.sql
new file mode 100644
index 0000000..41b0e44
--- /dev/null
+++ b/dbt/include/mariadb/macros/materializations/incremental/incremental.sql
@@ -0,0 +1,59 @@
+
+{% materialization incremental, adapter='mariadb' %}
+
+  {% set unique_key = config.get('unique_key') %}
+
+  {% set target_relation = this.incorporate(type='table') %}
+  {% set existing_relation = load_relation(this) %}
+  {% set tmp_relation = make_temp_relation(this) %}
+
+  {{ run_hooks(pre_hooks, inside_transaction=False) }}
+
+  -- `BEGIN` happens here:
+  {{ run_hooks(pre_hooks, inside_transaction=True) }}
+
+  {% set to_drop = [] %}
+  {% if existing_relation is none %}
+      {% set build_sql = create_table_as(False, target_relation, sql) %}
+  {% elif existing_relation.is_view or should_full_refresh() %}
+      {#-- Make sure the backup doesn't exist so we don't encounter issues with the rename below #}
+      {% set backup_identifier = existing_relation.identifier ~ "__dbt_backup" %}
+      {% set backup_relation = existing_relation.incorporate(path={"identifier": backup_identifier}) %}
+      {% do adapter.drop_relation(backup_relation) %}
+
+      {% do adapter.rename_relation(target_relation, backup_relation) %}
+      {% set build_sql = create_table_as(False, target_relation, sql) %}
+      {% do to_drop.append(backup_relation) %}
+  {% else %}
+      {% set tmp_relation = make_temp_relation(target_relation) %}
+      {% do run_query(create_table_as(True, tmp_relation, sql)) %}
+      {% do adapter.expand_target_column_types(
+             from_relation=tmp_relation,
+             to_relation=target_relation) %}
+      {% set build_sql = incremental_delete(tmp_relation, target_relation, unique_key=unique_key, statement_name="pre_main") %}
+      {% call statement("pre_main") %}
+          {{ build_sql }}
+      {% endcall %}
+      {% set build_sql = incremental_insert(tmp_relation, target_relation, unique_key=unique_key) %}
+  {% endif %}
+
+  {% call statement("main") %}
+      {{ build_sql }}
+  {% endcall %}
+
+  {% do persist_docs(target_relation, model) %}
+
+  {{ run_hooks(post_hooks, inside_transaction=True) }}
+
+  -- `COMMIT` happens here
+  {% do adapter.commit() %}
+
+  {% for rel in to_drop %}
+      {% do adapter.drop_relation(rel) %}
+  {% endfor %}
+
+  {{ run_hooks(post_hooks, inside_transaction=False) }}
+
+  {{ return({'relations': [target_relation]}) }}
+
+{%- endmaterialization %}
diff --git a/dbt/include/mariadb/macros/materializations/seed/seed.sql b/dbt/include/mariadb/macros/materializations/seed/seed.sql
new file mode 100644
index 0000000..3126930
--- /dev/null
+++ b/dbt/include/mariadb/macros/materializations/seed/seed.sql
@@ -0,0 +1,35 @@
+
+{% macro basic_load_csv_rows(model, batch_size, agate_table) %}
+    {% set cols_sql = get_seed_column_quoted_csv(model, agate_table.column_names) %}
+    {% set bindings = [] %}
+
+    {% set statements = [] %}
+
+    {% for chunk in agate_table.rows | batch(batch_size) %}
+        {% set bindings = [] %}
+
+        {% for row in chunk %}
+            {% do bindings.extend(row) %}
+        {% endfor %}
+
+        {% set sql %}
+            insert into {{ this.render() }} ({{ cols_sql }}) values
+            {% for row in chunk -%}
+                ({%- for column in agate_table.column_names -%}
+                    %s
+                    {%- if not loop.last%},{%- endif %}
+                {%- endfor -%})
+                {%- if not loop.last%},{%- endif %}
+            {%- endfor %}
+        {% endset %}
+
+        {% do adapter.add_query(sql, bindings=bindings, abridge_sql_log=True) %}
+
+        {% if loop.index0 == 0 %}
+            {% do statements.append(sql) %}
+        {% endif %}
+    {% endfor %}
+
+    {# Return SQL so we can render it out into the compiled files #}
+    {{ return(statements[0]) }}
+{% endmacro %}
diff --git a/dbt/include/mariadb/macros/materializations/snapshot/snapshot.sql b/dbt/include/mariadb/macros/materializations/snapshot/snapshot.sql
new file mode 100644
index 0000000..d09d9c0
--- /dev/null
+++ b/dbt/include/mariadb/macros/materializations/snapshot/snapshot.sql
@@ -0,0 +1,213 @@
+
+{% macro mariadb__snapshot_string_as_time(timestamp) -%}
+    {%- set result = "str_to_date('" ~ timestamp ~ "', '%Y-%m-%d %T')" -%}
+    {{ return(result) }}
+{%- endmacro %}
+
+{% materialization snapshot, adapter='mariadb' %}
+  {%- set config = model['config'] -%}
+
+  {%- set target_table = model.get('alias', model.get('name')) -%}
+
+  {%- set strategy_name = config.get('strategy') -%}
+  {%- set unique_key = config.get('unique_key') %}
+
+  {% if not adapter.check_schema_exists(model.database, model.schema) %}
+    {% do create_schema(model.database, model.schema) %}
+  {% endif %}
+
+  {% set target_relation_exists, target_relation = get_or_create_relation(
+          database=none,
+          schema=model.schema,
+          identifier=target_table,
+          type='table') -%}
+
+  {%- if not target_relation.is_table -%}
+    {% do exceptions.relation_wrong_type(target_relation, 'table') %}
+  {%- endif -%}
+
+  {{ run_hooks(pre_hooks, inside_transaction=False) }}
+
+  {{ run_hooks(pre_hooks, inside_transaction=True) }}
+
+  {% set strategy_macro = strategy_dispatch(strategy_name) %}
+  {% set strategy = strategy_macro(model, "snapshotted_data", "source_data", config, target_relation_exists) %}
+
+  {% if not target_relation_exists %}
+
+      {% set build_sql = build_snapshot_table(strategy, model['compiled_sql']) %}
+      {% set final_sql = create_table_as(False, target_relation, build_sql) %}
+
+      {% call statement('main') %}
+          {{ final_sql }}
+      {% endcall %}
+
+  {% else %}
+
+      {{ adapter.valid_snapshot_target(target_relation) }}
+
+      {% set staging_table = build_snapshot_staging_table(strategy, sql, target_relation) %}
+
+      -- this may no-op if the database does not require column expansion
+      {% do adapter.expand_target_column_types(from_relation=staging_table,
+                                               to_relation=target_relation) %}
+
+      {% set missing_columns = adapter.get_missing_columns(staging_table, target_relation)
+                                   | rejectattr('name', 'equalto', 'dbt_change_type')
+                                   | rejectattr('name', 'equalto', 'DBT_CHANGE_TYPE')
+                                   | rejectattr('name', 'equalto', 'dbt_unique_key')
+                                   | rejectattr('name', 'equalto', 'DBT_UNIQUE_KEY')
+                                   | list %}
+
+      {% do create_columns(target_relation, missing_columns) %}
+
+      {% set source_columns = adapter.get_columns_in_relation(staging_table)
+                                   | rejectattr('name', 'equalto', 'dbt_change_type')
+                                   | rejectattr('name', 'equalto', 'DBT_CHANGE_TYPE')
+                                   | rejectattr('name', 'equalto', 'dbt_unique_key')
+                                   | rejectattr('name', 'equalto', 'DBT_UNIQUE_KEY')
+                                   | list %}
+
+      {% set quoted_source_columns = [] %}
+      {% for column in source_columns %}
+        {% do quoted_source_columns.append(adapter.quote(column.name)) %}
+      {% endfor %}
+
+      -- The MERGE statement is not supported, so we need to use seperate UPDATE + INSERT statements instead
+      {% set final_sql_update = mariadb__snapshot_merge_sql_update(
+            target = target_relation,
+            source = staging_table,
+            insert_cols = quoted_source_columns
+         )
+      %}
+
+      {% set final_sql_insert = mariadb__snapshot_merge_sql_insert(
+            target = target_relation,
+            source = staging_table,
+            insert_cols = quoted_source_columns
+         )
+      %}
+
+      {% call statement('main') %}
+          {{ final_sql_update }}
+      {% endcall %}
+
+      {% call statement('main') %}
+          {{ final_sql_insert }}
+      {% endcall %}
+
+  {% endif %}
+
+  {% do persist_docs(target_relation, model) %}
+
+  {{ run_hooks(post_hooks, inside_transaction=True) }}
+
+  {{ adapter.commit() }}
+
+  {% if staging_table is defined %}
+      {% do post_snapshot(staging_table) %}
+  {% endif %}
+
+  {{ run_hooks(post_hooks, inside_transaction=False) }}
+
+  {{ return({'relations': [target_relation]}) }}
+
+{% endmaterialization %}
+
+{% macro snapshot_check_all_get_existing_columns(node, target_exists) -%}
+    {%- set query_columns = get_columns_in_query(node['compiled_sql']) -%}
+    {%- if not target_exists -%}
+        {# no table yet -> return whatever the query does #}
+        {{ return([false, query_columns]) }}
+    {%- endif -%}
+    {# handle any schema changes #}
+    {%- set target_table = node.get('alias', node.get('name')) -%}
+    {%- set target_relation = adapter.get_relation(database=None, schema=node.schema, identifier=target_table) -%}
+    {%- set existing_cols = get_columns_in_query('select * from ' ~ target_relation) -%}
+    {%- set ns = namespace() -%} {# handle for-loop scoping with a namespace #}
+    {%- set ns.column_added = false -%}
+
+    {%- set intersection = [] -%}
+    {%- for col in query_columns -%}
+        {%- if col in existing_cols -%}
+            {%- do intersection.append(col) -%}
+        {%- else -%}
+            {% set ns.column_added = true %}
+        {%- endif -%}
+    {%- endfor -%}
+    {{ return([ns.column_added, intersection]) }}
+{%- endmacro %}
+
+{% macro snapshot_staging_table(strategy, source_sql, target_relation) -%}
+
+    select
+        'insert' as dbt_change_type,
+        source_data.*
+
+    from (
+
+        select
+            *,
+            {{ strategy.unique_key }} as dbt_unique_key,
+            {{ strategy.updated_at }} as dbt_updated_at,
+            {{ strategy.updated_at }} as dbt_valid_from,
+            nullif({{ strategy.updated_at }}, {{ strategy.updated_at }}) as dbt_valid_to,
+            {{ strategy.scd_id }} as dbt_scd_id
+
+        from (
+            {{ source_sql }}
+        ) as snapshot_query
+
+    ) as source_data
+    left outer join (
+
+        select *,
+            {{ strategy.unique_key }} as dbt_unique_key
+
+        from {{ target_relation }}
+
+    ) as snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
+    where snapshotted_data.dbt_unique_key is null
+       or (
+            snapshotted_data.dbt_unique_key is not null
+        and snapshotted_data.dbt_valid_to is null
+        and (
+            {{ strategy.row_changed }}
+        )
+    )
+
+    union all
+
+    select
+        'update' as dbt_change_type,
+        source_data.*,
+        snapshotted_data.dbt_scd_id
+
+    from (
+
+        select
+            *,
+            {{ strategy.unique_key }} as dbt_unique_key,
+            {{ strategy.updated_at }} as dbt_updated_at,
+            {{ strategy.updated_at }} as dbt_valid_from,
+            {{ strategy.updated_at }} as dbt_valid_to
+
+        from (
+            {{ source_sql }}
+        ) as snapshot_query
+
+    ) as source_data
+    join (
+
+        select *,
+            {{ strategy.unique_key }} as dbt_unique_key
+
+        from {{ target_relation }}
+
+    ) as snapshotted_data on snapshotted_data.dbt_unique_key = source_data.dbt_unique_key
+    where snapshotted_data.dbt_valid_to is null
+    and (
+        {{ strategy.row_changed }}
+    )
+
+{%- endmacro %}
diff --git a/dbt/include/mariadb/macros/materializations/snapshot/snapshot_merge.sql b/dbt/include/mariadb/macros/materializations/snapshot/snapshot_merge.sql
new file mode 100644
index 0000000..495ad14
--- /dev/null
+++ b/dbt/include/mariadb/macros/materializations/snapshot/snapshot_merge.sql
@@ -0,0 +1,19 @@
+
+{% macro mariadb__snapshot_merge_sql_update(target, source, insert_cols) -%}
+    update {{ target }}, (select dbt_scd_id, dbt_change_type, dbt_valid_to from {{ source }}) as DBT_INTERNAL_SOURCE
+    set {{ target }}.dbt_valid_to = DBT_INTERNAL_SOURCE.dbt_valid_to
+    where DBT_INTERNAL_SOURCE.dbt_scd_id = {{ target }}.dbt_scd_id
+    and DBT_INTERNAL_SOURCE.dbt_change_type = 'update'
+    and {{ target }}.dbt_valid_to is null
+{% endmacro %}
+
+{% macro mariadb__snapshot_merge_sql_insert(target, source, insert_cols) -%}
+    {%- set insert_cols_csv = insert_cols | join(', ') -%}
+
+    insert into {{ target }} ({{ insert_cols_csv }})
+    select {% for column in insert_cols -%}
+        DBT_INTERNAL_SOURCE.{{ column }} {%- if not loop.last %}, {%- endif %}
+    {%- endfor %}
+    from {{ source }} as DBT_INTERNAL_SOURCE
+    where DBT_INTERNAL_SOURCE.dbt_change_type = 'insert'
+{% endmacro %}
diff --git a/dbt/include/mariadb/macros/materializations/snapshot/strategies.sql b/dbt/include/mariadb/macros/materializations/snapshot/strategies.sql
new file mode 100644
index 0000000..254c0a7
--- /dev/null
+++ b/dbt/include/mariadb/macros/materializations/snapshot/strategies.sql
@@ -0,0 +1,7 @@
+
+{% macro mariadb__snapshot_hash_arguments(args) -%}
+    md5(concat_ws('|', {%- for arg in args -%}
+        coalesce(cast({{ arg }} as char), '')
+        {% if not loop.last %}, {% endif %}
+    {%- endfor -%}))
+{%- endmacro %}
diff --git a/dbt/include/mariadb/sample_profiles.yml b/dbt/include/mariadb/sample_profiles.yml
new file mode 100644
index 0000000..1272a0d
--- /dev/null
+++ b/dbt/include/mariadb/sample_profiles.yml
@@ -0,0 +1,21 @@
+default:
+  target: dev
+  outputs:
+    dev:
+      type: mariadb
+      server: [server/host]
+      port: [port]  # optional
+      database: [schema] # optional, should be same as schema
+      schema: [schema]
+      username: [username]
+      password: [password]
+      driver: MySQL ODBC 8.0 ANSI Driver
+    prod:
+      type: mariadb
+      server: [server/host]
+      port: [port]  # optional
+      database: [schema] # optional, should be same as schema
+      schema: [schema]
+      username: [username]
+      password: [password]
+      driver: MySQL ODBC 8.0 ANSI Driver


Test patch:
diff --git a/test/integration/mariadb-10.5.dbtspec b/test/integration/mariadb-10.5.dbtspec
index 00c7682..d8d28ba 100644
--- a/test/integration/mariadb-10.5.dbtspec
+++ b/test/integration/mariadb-10.5.dbtspec
@@ -1,6 +1,6 @@
 
 target:
-  type: mysql
+  type: mariadb
   port: 3306
   database: dbt_test_{{ var('_dbt_random_suffix') }}
   schema: dbt_test_{{ var('_dbt_random_suffix') }}
@@ -15,10 +15,10 @@ sequences:
 
   test_dbt_empty: empty
   test_dbt_base: base
-  # test_dbt_ephemeral: ephemeral
+  test_dbt_ephemeral: ephemeral
   test_dbt_incremental: incremental
   # test_dbt_snapshot_strategy_timestamp: snapshot_strategy_timestamp
   # test_dbt_snapshot_strategy_check_cols: snapshot_strategy_check_cols
   test_dbt_data_test: data_test
   test_dbt_schema_test: schema_test
-  # test_dbt_ephemeral_data_tests: data_test_ephemeral_models
+  test_dbt_ephemeral_data_tests: data_test_ephemeral_models