Repository: BASIN-3D/basin3d (language: python)

Issue:
Refactor Metadata Catalog to Use SQLite and SQLAlchemy
The current implementation of the metadata catalog in the `basin3d` project uses TinyDB for in-memory storage. TinyDB is not being actively maintained, which poses a risk for future development and maintenance. To improve scalability, persistence, and performance, we need to refactor the catalog to use SQLite with SQLAlchemy as the ORM. This will involve removing the `CatalogTinyDb` class and replacing it with a new `CatalogSqlAlchemy` class.

**Tasks:**
1. **Remove `CatalogTinyDb` Implementation:**
   - Delete the `CatalogTinyDb` class from `basin3d/catalog.py`.
   - Remove any TinyDB-specific code and dependencies.

2. **Create `CatalogSqlAlchemy` Implementation:**
   - Define a new `CatalogSqlAlchemy` class in `basin3d/catalog.py`.
   - Set up SQLAlchemy models for `ObservedProperty`, `AttributeMapping`, and `DataSource`.
   - Implement methods for initializing the database, inserting records, and querying data.
   - Ensure the new class adheres to the same interface as the old `CatalogTinyDb` class.

3. **Update Initialization Logic:**
   - Modify the `initialize` method to set up the SQLite database using SQLAlchemy.
   - Ensure that the database schema is created if it does not exist.

4. **Implement CRUD Operations:**
   - Implement methods for creating, reading, updating, and deleting records in the SQLite database.
   - Ensure that all existing functionality is preserved.

5. **Testing:**
   - Write unit tests for the new `CatalogSqlAlchemy` class.
   - Ensure that all existing tests pass with the new implementation.
   - Add tests for any new functionality introduced during the refactor.

6. **Documentation:**
   - Update the project documentation to reflect the changes.
   - Provide examples of how to use the new `CatalogSqlAlchemy` class.

**Acceptance Criteria:**
- The `CatalogTinyDb` class is removed from the codebase.
- A new `CatalogSqlAlchemy` class is implemented and fully functional.
- All existing functionality is preserved and works as expected.
- The project passes all tests.
- Documentation is updated to reflect the changes.

**Additional Notes:**
- Ensure that the new implementation is efficient and leverages SQLAlchemy's capabilities for handling complex queries and relationships.
- Consider potential future scalability and performance improvements when designing the new class.


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
index 480f1d2..7b9ef45 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -3,6 +3,8 @@ exclude_lines =
    NotImplementedError
    logger
    def __repr__
+   def __unicode__
+   def __str__
    version
    release
    logger.info
@@ -10,6 +12,10 @@ exclude_lines =
    pass
    ImportError
    self.log
+   logger.error
+   logger.debug
+   except PackageNotFoundError:
+   except Exception
 omit =
    */asgi.py
    */formparser.py
diff --git a/basin3d/core/catalog.py b/basin3d/core/catalog.py
index 95d4396..ab82835 100644
--- a/basin3d/core/catalog.py
+++ b/basin3d/core/catalog.py
@@ -1,10 +1,10 @@
 """
 
-.. currentmodule:: basin3d.core.synthesis
+.. currentmodule:: basin3d.core.sqlalchemy_models
 
 :platform: Unix, Mac
 :synopsis: BASIN-3D ``DataSource`` catalog classes
-:module author: Val Hendrix <vhendrix@lbl.gov>
+:module author: Valerie C. Hendrix <vchendrix@lbl.gov>
 :module author: Danielle Svehla Christianson <dschristianson@lbl.gov>
 
 .. contents:: Contents
@@ -13,31 +13,47 @@
 
 """
 import csv
-from basin3d.core import monitor
+from sqlalchemy.exc import SQLAlchemyError, NoResultFound, IntegrityError
+
+import basin3d
+from basin3d.core import monitor, models
 from importlib import resources
 from inspect import getmodule
-import re
 from string import whitespace
-from typing import Dict, Iterator, List, Optional, Union
+from typing import Iterator, List, Optional, Union
 
-from basin3d.core.schema.enum import MAPPING_DELIMITER, NO_MAPPING_TEXT, MappedAttributeEnum, set_mapped_attribute_enum_type
-from basin3d.core.models import DataSource, AttributeMapping, ObservedProperty
+from basin3d.core.schema.enum import MAPPING_DELIMITER, NO_MAPPING_TEXT, MappedAttributeEnum, \
+    set_mapped_attribute_enum_type, BaseEnum
+from basin3d.core import sqlalchemy_models
 
 logger = monitor.get_logger(__name__)
+"""The logger for the catalog"""
+
+DATASOURCE_NONE = models.DataSource(name="", location="", id_prefix="", id="", credentials={})
+"""The default DataSource object when a DataSource is not found"""
 
 
 class CatalogException(Exception):
+    """The exception class for the Catalog"""
     pass
 
 
 class CatalogBase:
 
     def __init__(self, variable_filename: str = 'basin3d_observed_property_vocabulary.csv'):
-        self.variable_dir = 'basin3d.data'
-        self.variable_filename = variable_filename
+        """
+        Initialize the catalog
+
+        :param variable_filename: the filename of the observed property vocabulary
+        """
+        self.variable_dir = 'basin3d.data'  # location of the variable file
+        self.variable_filename = variable_filename  # observed property vocabulary filename
+        self._plugin_ids: List[str] = []  # list of plugin ids (updated on initialization)
 
     def initialize(self, plugin_list: list):
         """
+        Initialize the catalog. This method should be called before any other method.
+
 
         :param plugin_list: list of plugins
         :return: tinyDB object
@@ -45,16 +61,26 @@ def initialize(self, plugin_list: list):
         if not self.is_initialized():
             logger.debug(f"Initializing {self.__class__.__name__} metadata catalog ")
 
-            # initiate db
-            self._init_catalog()
+            def _get_plugin_id(plugin):
+                """Get the plugin id from the plugin"""
+                try:
+                    if isinstance(plugin, str):
+                        return basin3d.core.plugin.PluginMount.plugins[plugin].get_meta().id
+                    return plugin.get_meta().id
+                except Exception:
+                    logger.error(
+                        'Could not retrieve plugin_id. Check that plugin is configured / registered properly.')
+                    raise CatalogException
+
+            self._plugin_ids = plugin_list and [_get_plugin_id(plugin) for plugin in plugin_list if plugin] or []
 
-            if not plugin_list:
-                plugin_list = []
+            # initiate db
+            self._init_catalog(plugin_ids=self._plugin_ids)
 
             # generate variable store
             self._gen_variable_store()
 
-            # Load plugins
+            # Load plugins, get the mapping file, and insert into the catalog datastore
             for plugin in plugin_list:
 
                 try:
@@ -82,54 +108,53 @@ def initialize(self, plugin_list: list):
 
                 datasource = plugin.get_datasource()
                 self._process_plugin_attr_mapping(plugin, mapping_filename, datasource)
-                self._insert(datasource)
 
             logger.info(f"Initialized {self.__class__.__name__} metadata catalog ")
 
     # ---------------------------------
     # Methods to be over-witten by children classes
 
-    def _init_catalog(self):
+    def _init_catalog(self, **kwargs):
         """
+        Initialize the catalog database
 
-        :return:
         """
         raise NotImplementedError
 
     def _insert(self, record):
         """
         Insert the record
-        :param record:
+
+        :param record: the record to insert
         """
         raise NotImplementedError
 
-    def _get_datasource(self, datasource_id) -> Optional[DataSource]:
+    def _get_datasource(self, datasource_id) -> Optional[basin3d.core.models.DataSource]:
         """
         Access a single datasource
 
         :param datasource_id: the datasource_identifier
-        :return:
         """
         raise NotImplementedError
 
-    def _get_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
+    def _get_observed_property(self, basin3d_vocab) -> Optional[basin3d.core.models.ObservedProperty]:
         """
         Access a single observed property variable
 
         :param basin3d_vocab: the observed property variable identifier
-        :return:
         """
         raise NotImplementedError
 
-    def _get_attribute_mapping(self, datasource_id, attr_type, basin3d_vocab, datasource_vocab, **kwargs) -> Optional[AttributeMapping]:
+    def _get_attribute_mapping(self, datasource_id, attr_type, basin3d_vocab, datasource_vocab, **kwargs) -> Optional[
+            basin3d.core.models.AttributeMapping]:
         """
         Access a single attribute mapping
 
-        :param datasource_id:
-        :param attr_type:
-        :param basin3d_vocab:
-        :param datasource_vocab:
-        :return:
+        :param datasource_id: the datasource identifier, e.g., plugin_id
+        :param attr_type: the attribute type, e.g., STATISTIC
+        :param basin3d_vocab: the BASIN-3D vocabulary, e.g., Max
+        :param datasource_vocab: the datasource vocabulary, e.g., max
+        :return: the attribute mapping
         """
         raise NotImplementedError
 
@@ -137,7 +162,7 @@ def is_initialized(self) -> bool:
         """Has the catalog been initialized?"""
         raise NotImplementedError
 
-    def find_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
+    def find_observed_property(self, basin3d_vocab) -> Optional[basin3d.core.models.ObservedProperty]:
         """
         Return the :class:`basin3d.models.ObservedProperty` object for the BASIN-3D vocabulary specified.
 
@@ -146,7 +171,7 @@ def find_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
         """
         raise NotImplementedError
 
-    def find_observed_properties(self, basin3d_vocab=None) -> Iterator[Optional[ObservedProperty]]:
+    def find_observed_properties(self, basin3d_vocab=None) -> Iterator[Optional[basin3d.core.models.ObservedProperty]]:
         """
         Report the observed_properties available based on the BASIN-3D vocabularies specified. If no BASIN-3D vocabularies are specified, then return all observed properties available.
 
@@ -155,7 +180,8 @@ def find_observed_properties(self, basin3d_vocab=None) -> Iterator[Optional[Obse
         """
         raise NotImplementedError
 
-    def find_datasource_attribute_mapping(self, datasource_id, attr_type, attr_vocab) -> Optional[AttributeMapping]:
+    def find_datasource_attribute_mapping(self, datasource_id, attr_type, attr_vocab) -> Optional[
+            basin3d.core.models.AttributeMapping]:
         """
         Find the datasource attribute vocabulary to BASIN-3D mapping given a specific datasource_id, attr_type, and datasource attr_vocab.
 
@@ -166,13 +192,14 @@ def find_datasource_attribute_mapping(self, datasource_id, attr_type, attr_vocab
         """
         raise NotImplementedError
 
-    def find_attribute_mappings(self, datasource, attr_type, attr_vocab, from_basin3d) -> Iterator[AttributeMapping]:
+    def find_attribute_mappings(self, datasource, attr_type, attr_vocab, from_basin3d) -> Iterator[
+            basin3d.core.models.AttributeMapping]:
         """
         Find the list of attribute mappings given the specified fields.
         Exact matches are returned (see attr_vocab formats below for BASIN-3D vocab nuances).
         If no fields are specified, all registered attribute mappings will be returned.
 
-        :param datasource_id: the datasource identifier
+        :param datasource: the datasource object
         :param attr_type: the attribute type
         :param attr_vocab: the attribute vocabulary, the formats are one of the following:
                            1) datasource vocab that is a complete vocab
@@ -188,8 +215,8 @@ def find_attribute_mappings(self, datasource, attr_type, attr_vocab, from_basin3
 
     def _gen_variable_store(self):
         """
-        Generate a variable store. Loads this from the provided vocabulary CSV
-        :return:
+        Generate a variable store. Loads this from the provided vocabulary CSV file.
+
         """
         fields = ['basin3d_vocab', 'description', 'categories', 'units']
         # location of mapping file might change.
@@ -215,7 +242,7 @@ def _gen_variable_store(self):
                     categories_list = categories_str.split(',')
                     categories_list = [category.strip(whitespace) for category in categories_list]
 
-                observed_property_variable = ObservedProperty(
+                observed_property_variable = basin3d.core.models.ObservedProperty(
                     basin3d_vocab=basin3d_vocab,
                     full_name=row[fields[1]],
                     categories=categories_list,
@@ -226,6 +253,7 @@ def _gen_variable_store(self):
     def _get_attribute_enum(self, str_value, enum_type):
         """
         Get the enum for a given string value
+
         :param str_value: string value to convert to enum, e.g., Max
         :param enum_type: the enum type, e.g., StatisticEnum
         :return:
@@ -237,12 +265,13 @@ def _get_attribute_enum(self, str_value, enum_type):
             pass
         return enum_value
 
-    def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: DataSource):
+    def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: basin3d.core.models.DataSource):
         """
+        Process the plugin attribute mapping file
 
-        :param plugin:
-        :param filename:
-        :param datasource:
+        :param plugin: the plugin, e.g., the plugin object
+        :param filename: the filename of the mapping file, e.g., 'plugin_mapping.csv'
+        :param datasource: the datasource object
         :return:
         """
         fields = ['attr_type', 'basin3d_vocab', 'datasource_vocab', 'datasource_desc']
@@ -281,7 +310,8 @@ def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: DataSo
 
                 # build the basin3d_description from objects (e.g., for OBSERVED_PROPERTY) or enums
                 basin3d_desc = []
-                for a_type, b3d_vocab in zip(attr_type.split(MAPPING_DELIMITER), basin3d_vocab.split(MAPPING_DELIMITER)):
+                for a_type, b3d_vocab in zip(attr_type.split(MAPPING_DELIMITER),
+                                             basin3d_vocab.split(MAPPING_DELIMITER)):
                     valid_type_vocab = True
 
                     b3d_attr_type = None
@@ -293,12 +323,15 @@ def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: DataSo
                             b3d_enum_type = set_mapped_attribute_enum_type(b3d_attr_type)
                             b3d_desc = self._get_attribute_enum(b3d_vocab, b3d_enum_type)
                     except AttributeError:
-                        logger.warning(f'Datasource {datasource.id}: Attribute type {a_type} is not supported. Skipping mapping.')
+                        logger.warning(
+                            f'Datasource {datasource.id}: Attribute type {a_type} is not supported. Skipping mapping.')
                         valid_type_vocab = False
                         break
 
                     if not b3d_desc:
-                        logger.warning(f'Datasource {datasource.id}: basin3d_vocab {b3d_vocab} for attr_type {a_type} is not a valid BASIN-3D vocabulary. Skipping attribute mapping.')
+                        logger.warning(
+                            f'Datasource {datasource.id}: basin3d_vocab {b3d_vocab} for attr_type {a_type} '
+                            f'is not a valid BASIN-3D vocabulary. Skipping attribute mapping.')
                         valid_type_vocab = False
                         break
 
@@ -307,7 +340,7 @@ def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: DataSo
                 if not valid_type_vocab:
                     continue
 
-                attr_mapping = AttributeMapping(
+                attr_mapping = basin3d.core.models.AttributeMapping(
                     attr_type=attr_type,
                     basin3d_vocab=basin3d_vocab,
                     basin3d_desc=basin3d_desc,
@@ -318,157 +351,268 @@ def _process_plugin_attr_mapping(self, plugin, filename: str, datasource: DataSo
                 self._insert(attr_mapping)
                 logger.debug(f"{datasource.id}: Mapped {attr_type} {datasource_vocab} to {basin3d_vocab}")
 
-    def _validate_find_attribute_mappings_arguments(self, datasource_id: str = None, attr_type: str = None):
-        """
-        Validate arguments for method find_attribute_mappings
-
-        :param datasource_id: datasource ID
-        :param attr_type: Attribute Type
-        """
-        if datasource_id and self._get_datasource(datasource_id) is None:
-            msg = f'Specified datasource_id: "{datasource_id}" has not been registered.'
-            logger.critical(msg)
-            raise CatalogException(msg)
-
-        if attr_type and attr_type not in MappedAttributeEnum.values():
-            msg = f'"{attr_type}" is not an attribute type supported by BASIN-3D.'
-            logger.critical(msg)
-            raise CatalogException(msg)
 
-
-class CatalogTinyDb(CatalogBase):
+class CatalogSqlAlchemy(CatalogBase):
 
     def __init__(self, variable_filename: str = 'basin3d_observed_property_vocabulary.csv'):
         super().__init__(variable_filename)
 
-        self.in_memory_db = None
-        self.in_memory_db_attr = None
-        self._observed_properties: Dict[str, ObservedProperty] = {}
-        self._attribute_mappings: Dict[str, AttributeMapping] = {}
-        self._datasources: Dict[str, DataSource] = {}
+        # Clear the database, on initialization
+        basin3d.core.sqlalchemy_models.clear_database()
 
     def is_initialized(self) -> bool:
         """Has the catalog been initialized?"""
-        return self.in_memory_db is not None
 
-    def _get_datasource(self, datasource_id) -> Optional[DataSource]:
+        session = sqlalchemy_models.Session()
+
+        try:
+            datasources = session.query(sqlalchemy_models.DataSource).count()
+            if isinstance(datasources, int):
+                return datasources > 0
+            logger.debug('Catalog not initialized')
+            return False
+        except SQLAlchemyError as e:
+            logger.error(f'SQLAlchemy error: {e}')
+            return False
+        finally:
+            session.close()
+
+    def _convert_observed_property(self, sqlalchemy_opv) -> Optional[basin3d.core.models.ObservedProperty]:
         """
-        Access a datasource via the datasource_id
-        :param datasource_id: str, the datasource id
-        :return: a :class:`basin3d.models.DataSource` object
+        Convert SQL Alchemy observed property variable to basin3d
+
+        :param sqlalchemy_opv:
+        :return: basin3d observed property
+        """
+
+        return basin3d.core.models.ObservedProperty(
+            basin3d_vocab=sqlalchemy_opv.basin3d_vocab,
+            full_name=sqlalchemy_opv.full_name,
+            categories=sqlalchemy_opv.categories.split(","),
+            units=sqlalchemy_opv.units
+        )
+
+    def _convert_attribute_mapping(self, sqlalchemy_am) -> Optional[basin3d.core.models.AttributeMapping]:
         """
-        return self._datasources.get(datasource_id, None)
+        Convert SQL Alchemy attribute_mapping
 
-    def _get_observed_property(self, basin3d_vocab) -> Optional[ObservedProperty]:
+        :param sqlalchemy_am: the sqlalchemy attribute mapping
+        :return: the basin3d attribute mapping
         """
-        Access a single observed property
 
-        :param basin3d_vocab: str, the observed property identifier
-        :return: an :class:`basin3d.models.ObservedProperty` object
+        attr_type_list = sqlalchemy_am.attr_type.split(MAPPING_DELIMITER)
+
+        basin3d_desc_list = []
+        if isinstance(sqlalchemy_am.basin3d_desc, list):
+            basin3d_desc_list = sqlalchemy_am.basin3d_desc
+
+        basin3d_desc = []
+
+        for attr_type, desc in zip(attr_type_list, basin3d_desc_list):
+            if attr_type == MappedAttributeEnum.OBSERVED_PROPERTY.value:
+                op = basin3d.core.models.ObservedProperty(
+                    basin3d_vocab=desc.get('basin3d_vocab'),
+                    full_name=desc.get('full_name'),
+                    categories=desc.get('categories'),
+                    units=desc.get('units')
+                )
+                basin3d_desc.append(op)
+            elif attr_type in MappedAttributeEnum.values():
+                attr_enum_class = set_mapped_attribute_enum_type(attr_type)
+                attr_type_enum = getattr(attr_enum_class, desc)
+                basin3d_desc.append(attr_type_enum)
+            else:
+                basin3d_desc.append(desc)
+
+        return basin3d.core.models.AttributeMapping(
+            attr_type=sqlalchemy_am.attr_type,
+            basin3d_vocab=sqlalchemy_am.basin3d_vocab,
+            basin3d_desc=basin3d_desc,
+            datasource_vocab=sqlalchemy_am.datasource_vocab,
+            datasource_desc=sqlalchemy_am.datasource_desc,
+            datasource=basin3d.core.models.DataSource(id=sqlalchemy_am.datasource.plugin_id,
+                                                      name=sqlalchemy_am.datasource.name,
+                                                      location=sqlalchemy_am.datasource.location,
+                                                      id_prefix=sqlalchemy_am.datasource.id_prefix, )
+        )
+
+    def _convert_basin3d_attr_mapping_basin3d_desc(self, basin3d_desc: list) -> list:
+        """
+        Convert the basin3d_desc to a JSON ready format
+
+        :param basin3d_desc: the basin3d_desc, a list of observed properties, enums, or strings
+        :return: a list of JSON ready basin3d_desc
+        """
+        json_ready_basin3d_desc = []
+
+        for desc in basin3d_desc:
+            if isinstance(desc, basin3d.core.models.ObservedProperty):
+                json_ready_basin3d_desc.append(desc.to_dict())
+            elif isinstance(desc, BaseEnum):
+                json_ready_basin3d_desc.append(desc.value)
+            else:
+                json_ready_basin3d_desc.append(desc)
+
+        return json_ready_basin3d_desc
+
+    def _get_observed_property(self, basin3d_vocab) -> Optional[basin3d.core.models.ObservedProperty]:
         """
-        return self._observed_properties.get(basin3d_vocab, None)
+        Access a single observed property variable
 
-    def _get_attribute_mapping(self, datasource_id, attr_type, basin3d_vocab, datasource_vocab, **kwargs) -> Optional[AttributeMapping]:
+        :param basin3d_vocab: the observed property name
+        :return: the observed property, or None if not found
         """
-        Access a single attribute mapping
+        session = sqlalchemy_models.Session()
 
-        :param datasource_id: str, datasource identifier
-        :param attr_type: str, attribute type
-        :param basin3d_vocab: str, BASIN-3D vocabulary
-        :param datasource_vocab: str, datasource vocabulary
-        :return: a :class:`basin3d.models.AttributeMapping` object
+        try:
+            opv = session.query(sqlalchemy_models.ObservedProperty).filter_by(basin3d_vocab=basin3d_vocab).one_or_none()
+            if opv:
+                return self._convert_observed_property(opv)
+            return None
+        except SQLAlchemyError as e:
+            if not isinstance(e, NoResultFound):
+                raise e
+            return None
+        finally:
+            session.close()
+
+    def _get_attribute_mapping(self, datasource_id, attr_type, basin3d_vocab, datasource_vocab, **kwargs) -> Optional[
+            basin3d.core.models.AttributeMapping]:
+        """
+        Access a single attribute mapping. If not found, return None.
+
+        :param datasource_id: the datasource identifier, e.g., plugin_id
+        :param attr_type: the attribute type, e.g., STATISTIC
+        :param basin3d_vocab: the BASIN-3D vocabulary, e.g., Max
+        :param datasource_vocab: the datasource vocabulary, e.g., max
+        :param kwargs: additional keyword arguments. Not used.
+
+        :return: the attribute mapping, or None if not found
         """
-        return self._attribute_mappings.get(f'{datasource_id}-{attr_type}-{basin3d_vocab}-{datasource_vocab}', None)
+        if not self.is_initialized():
+            raise CatalogException("Datasource catalog has not been initialized")
+
+        session = sqlalchemy_models.Session()
 
-    def find_observed_property(self, basin3d_vocab: str) -> Optional[ObservedProperty]:
+        try:
+            opv = session.query(sqlalchemy_models.AttributeMapping).filter_by(
+                datasource_id=datasource_id, attr_type=attr_type, basin3d_vocab=basin3d_vocab,
+                datasource_vocab=datasource_vocab).one_or_none()
+            if opv:
+                return self._convert_attribute_mapping(opv)
+            return None
+        except SQLAlchemyError as e:
+            if not isinstance(e, NoResultFound):
+                raise e
+            return None
+        finally:
+            session.close()
+
+    def find_observed_property(self, basin3d_vocab) -> Optional[basin3d.core.models.ObservedProperty]:
         """
         Return the :class:`basin3d.models.ObservedProperty` object for the BASIN-3D vocabulary specified.
 
         :param basin3d_vocab: BASIN-3D vocabulary
         :return: a :class:`basin3d.models.ObservedProperty` object
         """
-
-        if not self._observed_properties:
+        if not self.is_initialized():
             msg = "Variable Store has not been initialized."
             logger.critical(msg)
             raise CatalogException(msg)
 
         return self._get_observed_property(basin3d_vocab)
 
-    def find_observed_properties(self, basin3d_vocab: Optional[List[str]] = None) -> Iterator[Optional[ObservedProperty]]:
+    def find_observed_properties(self, basin3d_vocab: Optional[List[str]] = None) -> Iterator[
+            Optional[basin3d.core.models.ObservedProperty]]:
         """
         Report the observed_properties available based on the BASIN-3D vocabularies specified. If no BASIN-3D vocabularies are specified, then return all observed properties available.
 
         :param basin3d_vocab: list of the BASIN-3D observed properties
         :return: generator that yields :class:`basin3d.models.ObservedProperty` objects
         """
-
-        if not self._observed_properties:
+        if not self.is_initialized():
             msg = "Variable Store has not been initialized."
             logger.critical(msg)
             raise CatalogException(msg)
 
-        opv: Optional[ObservedProperty]
-        if basin3d_vocab is None:
-            for opv in self._observed_properties.values():
-                yield opv
-        else:
-            for b3d_vocab in basin3d_vocab:
-                opv = self._get_observed_property(b3d_vocab)
-                if opv is not None:
-                    yield opv
-                else:
-                    logger.warning(f'BASIN-3D does not support variable {b3d_vocab}')
+        session = sqlalchemy_models.Session()
 
-    def find_datasource_attribute_mapping(self, datasource_id: str, attr_type: str, datasource_vocab: str) -> Optional[AttributeMapping]:
+        try:
+            if not basin3d_vocab:
+                for opv in session.query(sqlalchemy_models.ObservedProperty).all():
+                    yield self._convert_observed_property(opv)
+            else:
+                for b3d_vocab in basin3d_vocab:
+                    b3d_opv: Optional[basin3d.core.models.ObservedProperty] = self._get_observed_property(b3d_vocab)
+                    if b3d_opv is not None:
+                        yield b3d_opv
+                    else:
+                        logger.warning(f'BASIN-3D does not support variable {b3d_vocab}')
+        finally:
+            session.close()
+
+    def find_datasource_attribute_mapping(self, datasource_id: str, attr_type: str, datasource_vocab: str) -> Optional[
+            basin3d.core.models.AttributeMapping]:
         """
         Find the datasource attribute vocabulary to BASIN-3D mapping given a specific datasource_id, attr_type, and datasource attr_vocab.
 
-        :param: datasource_id: the datasource identifier
-        :param: attr_type: attribute type
-        :param: datasource_vocab: the datasource attribute vocabulary
+        :param datasource_id: the datasource identifier
+        :param attr_type: attribute type
+        :param datasource_vocab: the datasource attribute vocabulary
         :return: a :class:`basin3d.models.AttributeMapping` object
+
         """
-        if self.in_memory_db_attr is None:
+        if not self.is_initialized():
             msg = 'Attribute Store has not been initialized.'
             logger.critical(msg)
             raise CatalogException(msg)
 
-        from tinydb import Query
-        query = Query()
+        session = sqlalchemy_models.Session()
 
-        # attr_type is search to accommodate compound mappings
-        results = self.in_memory_db_attr.search(
-            (query.datasource_vocab == datasource_vocab) & (query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
+        msg = (f'No mapping was found for attr: "{attr_type}" and for datasource vocab: "{datasource_vocab}" '
+               f'in datasource: "{datasource_id}".')
 
-        # Only one result should be returned. If so return it and all is good.
-        if len(results) == 1:
-            return self._get_attribute_mapping(**results[0])
-        # If not, deal with results:
-        # Case 1: more than one mapping for the datasource vocabulary. This should not happen. Raise an exception.
-        elif len(results) > 1:
-            error_msg = (f'More than one attribute mapping found for datasource vocab: "{datasource_vocab}" '
-                         f'in datasource: "{datasource_id}". This should never happen.')
-            logger.critical(error_msg)
-            raise CatalogException(error_msg)
-
-        # Case 2: no mapping was found. Return an empty AttributeMapping object
-        msg = f'No mapping was found for datasource vocab: "{datasource_vocab}" in datasource: "{datasource_id}".'
-        datasource = self._get_datasource(datasource_id)
-
-        if datasource is None:
-            datasource = DataSource()
-            msg = f'No datasource was found for id "{datasource_id}".'
-            logger.warning(msg)
-
-        return AttributeMapping(attr_type=attr_type, basin3d_vocab=NO_MAPPING_TEXT, basin3d_desc=[],
-                                datasource_vocab=datasource_vocab, datasource_desc=msg, datasource=datasource)
-
-    def find_attribute_mappings(self, datasource_id: str = None, attr_type: str = None, attr_vocab: Union[str, List] = None,
-                                from_basin3d: bool = False) -> Iterator[AttributeMapping]:
-        """
-        Find the list of attribute mappings given the specified fields.
-        Exact matches are returned (see attr_vocab formats below for BASIN-3D vocab nuances).
+        try:
+            datasource = session.query(sqlalchemy_models.DataSource).filter_by(plugin_id=datasource_id).one_or_none()
+            opv = None
+            query_params = {}
+            if datasource is None:
+                msg = f'No datasource was found for id "{datasource_id}".'
+                basin3d_datasource = DATASOURCE_NONE
+            else:
+                # Set up the search parameters
+                query_params = {
+                    'datasource_id': datasource.id,
+                    'datasource_vocab': datasource_vocab
+                }
+                basin3d_datasource = \
+                    basin3d.core.models.DataSource(
+                        id=datasource.plugin_id, name=datasource.name, location=datasource.location, id_prefix=datasource.id_prefix)  # type: ignore
+
+            # Set up empty AttributeMapping in case where mapping is not found or another error occurs
+            attr_mapping = basin3d.core.models.AttributeMapping(attr_type=attr_type, basin3d_vocab=NO_MAPPING_TEXT,
+                                                                basin3d_desc=[],
+                                                                datasource_vocab=datasource_vocab, datasource_desc=msg,
+                                                                datasource=basin3d_datasource)
+            if datasource:
+                opv = session.query(sqlalchemy_models.AttributeMapping).filter_by(**query_params).filter(
+                    sqlalchemy_models.AttributeMapping.attr_type.contains(attr_type)).one_or_none()
+            if opv is None:
+                return attr_mapping
+            return self._convert_attribute_mapping(opv)
+        except SQLAlchemyError as e:
+            if not isinstance(e, NoResultFound):
+                raise e
+            return attr_mapping
+        finally:
+            session.close()
+
+    def find_attribute_mappings(self, datasource_id: str = None, attr_type: str = None,
+                                attr_vocab: Union[str, List] = None,
+                                from_basin3d: bool = False) -> Iterator[basin3d.core.models.AttributeMapping]:
+        """
+        Find the list of attribute mappings given the specified fields. Exact matches are returned
+        (see attr_vocab formats below for BASIN-3D vocab nuances).
         If no fields are specified, all registered attribute mappings will be returned.
 
         :param datasource_id: the datasource identifier
@@ -479,170 +623,200 @@ def find_attribute_mappings(self, datasource_id: str = None, attr_type: str = No
                            3) BASIN-3D vocab that is compound and fully specified for each attr_type either with the complete vocab or with wildcards.
         :param from_basin3d: boolean that says whether the attr_vocab is a BASIN-3D vocabulary. If not, then this a datasource vocabulary.
         :return: generator that yields :class:`basin3d.models.AttributeMapping` objects
-        """
-        catalog_messages: List[str] = []
 
-        self._validate_find_attribute_mappings_arguments(datasource_id, attr_type)
+        """
 
-        if self.in_memory_db_attr is None:
+        if not self.is_initialized():
             msg = 'Attribute Store has not been initialized.'
             logger.critical(msg)
             raise CatalogException(msg)
 
-        from tinydb import Query
-        query = Query()
+        def construct_attr_vocab_query(attr_vocab_list, is_from_basin3d):
+            from sqlalchemy import or_
+            query = []
+            for a_vocab in attr_vocab_list:
+                if not is_from_basin3d:
+                    query.append(sqlalchemy_models.AttributeMapping.datasource_vocab == a_vocab)
+                elif MAPPING_DELIMITER in a_vocab:
+                    query.append(sqlalchemy_models.AttributeMapping.basin3d_vocab.op('regexp')(a_vocab))
+                else:
+                    query.append(or_(
+                        sqlalchemy_models.AttributeMapping.basin3d_vocab == a_vocab,
+                        sqlalchemy_models.AttributeMapping.basin3d_vocab.op('regexp')(f'.*:{a_vocab}'),
+                        sqlalchemy_models.AttributeMapping.basin3d_vocab.op('regexp')(f'{a_vocab}:.*'),
+                        sqlalchemy_models.AttributeMapping.basin3d_vocab.op('regexp')(f'.*:{a_vocab}:.*')
+                    ))
+            return or_(*query)
+
+        session = sqlalchemy_models.Session()
+        query_params = []
+
+        if datasource_id is not None:
+            try:
+                ds = session.query(sqlalchemy_models.DataSource).filter_by(plugin_id=datasource_id).one_or_none()
+                if ds is None:
+                    logger.warning(
+                        f'No datasource for datasource_id {datasource_id} was found. Check plugin initialization')
+                    raise CatalogException(f'No datasource for datasource_id {datasource_id} was found.')
+                else:
+                    query_params.append(sqlalchemy_models.AttributeMapping.datasource_id == ds.id)
+            except SQLAlchemyError as e:
+                if not isinstance(e, NoResultFound):
+                    raise CatalogException(e)
+
+        if attr_type is not None:
+            if attr_type not in MappedAttributeEnum.values():
+                logger.warning(f'Attribute type {attr_type} is invalid')
+                raise CatalogException(f'Attribute type {attr_type} is invalid')
+            else:
+                query_params.append(sqlalchemy_models.AttributeMapping.attr_type.contains(attr_type))
 
-        # Set the attribute vocab list to track mappings not found
-        specified_attr_vocab_not_found = []
         if attr_vocab:
             if isinstance(attr_vocab, str):
                 attr_vocab = [attr_vocab]
-            elif not isinstance(attr_vocab, List):
+            elif not isinstance(attr_vocab, list):
                 raise CatalogException("attr_vocab must be a str or list")
-            specified_attr_vocab_not_found = attr_vocab.copy()
-
-        # Function for TinyDB search
-        def is_in(x, attr_vocabs=attr_vocab, is_from_basin3d=from_basin3d):
-            for a_vocab in attr_vocabs:
-                # if attr_vocabs is datasource vocab(s)
-                # OR attr_vocabs is BASIN-3D vocab(s) AND they have the compound mapping delimiter (i.e., it has : and possibly wildcards)
-                # AND it is a fullmatch
-                if (not is_from_basin3d or (is_from_basin3d and MAPPING_DELIMITER in a_vocab)) and re.fullmatch(a_vocab, x):
-                    return True
-                # if attr_vocabs is BASIN-3D vocab(s) and they do not have the compound mapping delimiter,
-                # then match exactly for the individual attr_type components.
-                # e.g. attr_vocabs = ['MAX', 'MIN'] and the mapping is compound e.g. x = 'Al:WATER:MAX'
-                # re.search does not work here b/c of a case like OBSERVED_PROPERPTY variables TEMP:AIR and SONIC_TEMP:AIR,
-                # in which a attr_vocab of ['TEMP'] would return both instead on just TEMP:AIR.
-                # A general search would need to be handled differently.
-                elif is_from_basin3d and MAPPING_DELIMITER not in a_vocab:
-                    for x_vocab in x.split(MAPPING_DELIMITER):
-                        if x_vocab == a_vocab:
-                            return True
-            return False
-
-        # If no query parameters --> get all mapped attributes back for all registered plugins
-        if not datasource_id and not attr_vocab and not attr_type:
-            # return all mapped attributes possible possible
-            for attr_mapping in self._attribute_mappings.values():
-                yield attr_mapping
-        # Otherwise search depends on the set of parameters provided
-        else:
-            if not datasource_id:
-                if attr_type and attr_vocab and from_basin3d:
-                    results = self.in_memory_db_attr.search((query.basin3d_vocab.test(is_in)) & (query.attr_type.search(attr_type)))
-                elif attr_type and attr_vocab:
-                    results = self.in_memory_db_attr.search((query.datasource_vocab.test(is_in)) & (query.attr_type.search(attr_type)))
-                elif not attr_type and from_basin3d:
-                    results = self.in_memory_db_attr.search(query.basin3d_vocab.test(is_in))
-                elif not attr_type:
-                    results = self.in_memory_db_attr.search(query.datasource_vocab.test(is_in))
-                else:
-                    results = self.in_memory_db_attr.search(query.attr_type.search(attr_type))
-
-            # Returns all possible attributes for a data source
-            elif not attr_type and not attr_vocab:
-                results = self.in_memory_db_attr.search(query.datasource_id == datasource_id)
+            attr_vocab_query = construct_attr_vocab_query(attr_vocab, from_basin3d)
+            query_params.append(attr_vocab_query)
 
-            # Returns all attributes for a data source and attribute type
-            elif not attr_vocab:
-                results = self.in_memory_db_attr.search((query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
-
-            # Return mappings if only the datasource and attr_vocab are specified.
-            elif not attr_type:
+        try:
+            attr_mappings = session.query(sqlalchemy_models.AttributeMapping).filter(*query_params).all()
+            vocab_source_type = 'datasource' if not from_basin3d else 'BASIN-3D'
+
+            if not attr_mappings:
+                logger.info(
+                    f'No attribute mappings found for specified parameters: datasource id = '
+                    f'"{datasource_id}", attribute type = "{attr_type}", {vocab_source_type} '
+                    f'vocabularies: {attr_vocab and ",".join(attr_vocab) or None}.')
+            elif attr_vocab and len(attr_mappings) != len(attr_vocab):
+                # Find missing vocab in attr_mappings
                 if from_basin3d:
-                    results = self.in_memory_db_attr.search((query.basin3d_vocab.test(is_in)) & (query.datasource_id == datasource_id))
+                    not_found_attr_vocab = [vocab for vocab in attr_vocab if
+                                            vocab not in [am.basin3d_vocab for am in attr_mappings]]
                 else:
-                    results = self.in_memory_db_attr.search((query.datasource_vocab.test(is_in)) & (query.datasource_id == datasource_id))
+                    not_found_attr_vocab = [vocab for vocab in attr_vocab if
+                                            vocab not in [am.datasource_vocab for am in attr_mappings]]
+                logger.warning(
+                    f'No attribute mappings found for the following {vocab_source_type} '
+                    f'vocabularies: {",".join(not_found_attr_vocab)}. Note: specified datasource id = {datasource_id} and attribute type = {attr_type}.')
+        except SQLAlchemyError as e:
+            if not isinstance(e, NoResultFound):
+                raise e
 
-            # Finally, all parameters specified:
-            # Convert from BASIN-3D to DataSource variable name
-            elif from_basin3d:
-                results = self.in_memory_db_attr.search(
-                    (query.basin3d_vocab.test(is_in)) & (query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
+        for attr_mapping in attr_mappings:
+            value = self._convert_attribute_mapping(attr_mapping)
+            if value:
+                yield value
 
-            # Convert from DataSource variable name to BASIN-3D
-            else:
-                results = self.in_memory_db_attr.search(
-                    (query.datasource_vocab.test(is_in)) & (query.datasource_id == datasource_id) & (query.attr_type.search(attr_type)))
+        session.close()
 
-            # Yield the results
-            attr_map: Optional[AttributeMapping]
-            for r in results:
-                attr_map = self._get_attribute_mapping(**r)
+    def _init_catalog(self, **kwargs):
+        """
+        Initialize the catalog database
+        """
+        if not self.is_initialized():
+            session = sqlalchemy_models.Session()
 
-                # if AttributeMapping is not found: THIS SHOULD NEVER HAPPEN.
-                if attr_map is None:
-                    logger.error('AttributeMapping not found given database search results. THIS SHOULD NEVER HAPPPEN.')
-                    continue
+            from basin3d.core.plugin import PluginMount
+            for name, plugin in PluginMount.plugins.items():
 
-                # If attr_vocab was specified, track what was found for the datasource
-                if attr_vocab:
-                    vocabs = [attr_map.datasource_vocab]
-                    if from_basin3d:
-                        vocabs = attr_map.basin3d_vocab.split(MAPPING_DELIMITER)
-
-                    # look for it in the specified vocabs, remove it from the not found list
-                    for vocab in vocabs:
-                        try:
-                            idx = specified_attr_vocab_not_found.index(vocab)
-                            specified_attr_vocab_not_found.pop(idx)
-                        except ValueError:
-                            pass
-
-                # yield the AttributeMapping
-                yield attr_map
-
-            # If any specified attr_vocabs were not found, add info messages
-            if specified_attr_vocab_not_found:
-                attr_vocab_text = ', '.join(specified_attr_vocab_not_found)
-                attr_vocab_source = 'datasource'
-                if from_basin3d:
-                    attr_vocab_source = 'BASIN-3D'
-                if not results:
-                    # if no results,
-                    msg = (f'No attribute mappings found for specified parameters: datasource id = "{datasource_id}", '
-                           f'attribute type = "{attr_type}", {attr_vocab_source} vocabularies: {attr_vocab_text}.')
-                else:
-                    msg = (f'No attribute mappings found for the following {attr_vocab_source} vocabularies: {attr_vocab_text}. '
-                           f'Note: specified datasource id = {datasource_id} and attribute type = {attr_type}')
-                logger.info(msg)
-                catalog_messages.append(msg)
+                # Were the plugins passed in? If so, only load the plugins that are in the list
+                if ("plugin_ids" in kwargs and plugin.get_meta().id in kwargs[
+                   "plugin_ids"]) or "plugin_ids" not in kwargs or kwargs["plugin_ids"] == []:
+                    module_name = plugin.__module__
+                    class_name = plugin.__name__
 
-            return StopIteration(catalog_messages)
+                    logger.info("Loading Plugin = {}.{}".format(module_name, class_name))
 
-    def _init_catalog(self):
-        """
-        Initialize the catalog database
+                    try:
+                        datasource = session.query(sqlalchemy_models.DataSource).filter_by(
+                            plugin_id=plugin.get_meta().id).one_or_none()
+                        if datasource is None:
+                            logger.info("Registering NEW Data Source Plugin '{}.{}'".format(module_name, class_name))
+                            datasource = sqlalchemy_models.DataSource()
+                            if hasattr(plugin.get_meta(), "connection_class"):
+                                datasource.credentials = plugin.get_meta().connection_class.get_credentials_format()
+
+                        # Update the datasource
+                        datasource.plugin_id = plugin.get_meta().id
+                        datasource.name = plugin.get_meta().name
+                        datasource.location = plugin.get_meta().location
+                        datasource.id_prefix = plugin.get_meta().id_prefix
+                        datasource.plugin_module = module_name
+                        datasource.plugin_class = class_name
+                        session.add(datasource)
+                        session.commit()
+                        logger.info("Updated Data Source '{}'".format(plugin.get_meta().id))
+                    except SQLAlchemyError as e:
+                        session.rollback()
+                        logger.error(f"Error loading plugin {module_name}.{class_name}: {e}")
+                    finally:
+                        session.close()
 
-        :return:
+    def _insert(self, record):
         """
-        # Note: op are held in dictionary in this TinyDB
-        from tinydb import TinyDB
-        from tinydb.storages import MemoryStorage
-        self.in_memory_db = TinyDB(storage=MemoryStorage)
-        self.in_memory_db_attr = self.in_memory_db.table('attr')
-        self.in_memory_db_attr.truncate()
+        Insert the record. This method is used to insert the record into the database.
 
-    def _insert(self, record):
+
+        :param record: the record to insert
         """
-        :param record:
-        """
-        if self.in_memory_db is not None:
-            if isinstance(record, ObservedProperty):
-                self._observed_properties[record.basin3d_vocab] = record
-            elif isinstance(record, AttributeMapping):
-                key = f'{record.datasource.id}-{record.attr_type}-{record.basin3d_vocab}-{record.datasource_vocab}'
-                logger.debug(key)
-                self._attribute_mappings[key] = record
-                self.in_memory_db_attr.insert({'datasource_id': record.datasource.id,
-                                               'attr_type': record.attr_type,
-                                               'basin3d_vocab': record.basin3d_vocab,
-                                               'basin3d_desc': record.basin3d_desc,
-                                               'datasource_vocab': record.datasource_vocab,
-                                               'datasource_desc': record.datasource_desc, })
-            elif isinstance(record, DataSource):
-                self._datasources[record.id] = record
+        if self.is_initialized():
+            session = sqlalchemy_models.Session()
+            try:
+                if isinstance(record, basin3d.core.models.ObservedProperty):
+                    try:
+                        p = sqlalchemy_models.ObservedProperty(
+                            basin3d_vocab=record.basin3d_vocab,
+                            full_name=record.full_name,
+                            categories=",".join(record.categories),  # type: ignore
+                            units=record.units
+                        )
+                        session.add(p)
+                        session.commit()
+                        logger.info(f'inserted {record.basin3d_vocab}')
+                    except IntegrityError as ie:
+                        # This object has already been loaded
+                        session.rollback()
+                        logger.debug(f'Integrity error for OP: {ie}')
+                    except Exception as e:
+                        session.rollback()
+                        logger.warning(f"Error Registering ObservedProperty '{record.basin3d_vocab}': {str(e)}")
+
+                elif isinstance(record, basin3d.core.models.AttributeMapping):
+                    try:
+                        ds_name = session.query(sqlalchemy_models.DataSource).filter_by(
+                            plugin_id=record.datasource.id).one_or_none()
+                        if ds_name is None:
+                            raise ValueError(f"DataSource with id {record.datasource.plugin_id} not found")
+
+                        record_basin3d_desc = self._convert_basin3d_attr_mapping_basin3d_desc(record.basin3d_desc)
+
+                        p = sqlalchemy_models.AttributeMapping(
+                            datasource=ds_name,
+                            attr_type=record.attr_type,
+                            basin3d_vocab=record.basin3d_vocab,
+                            basin3d_desc=record_basin3d_desc,
+                            datasource_vocab=record.datasource_vocab,
+                            datasource_desc=record.datasource_desc
+                        )
+                        session.add(p)
+                        session.commit()
+                        logger.info(f'inserted {record.datasource_vocab} mapping attribute')
+                    except IntegrityError:
+                        # This object has already been loaded
+                        session.rollback()
+                        logger.info(f'Warning: skipping AttributeMapping "{record.basin3d_vocab}". Already loaded.')
+                    except Exception as e:
+                        session.rollback()
+                        logger.info(f'Error Registering AttributeMapping "{record.basin3d_vocab}": {str(e)}')
+                elif isinstance(record, basin3d.core.models.DataSource):
+                    pass  # Do nothing and don't error out
+                else:
+                    msg = f"Record type {type(record)} not supported for insertion into a {self.__class__.__name__}"
+                    logger.critical(msg)
+                    raise CatalogException(msg)
+            finally:
+                session.close()
         else:
             msg = 'Could not insert record. Catalog not initialized.'
             logger.critical(msg)
diff --git a/basin3d/core/plugin.py b/basin3d/core/plugin.py
index 0bba3f8..412f621 100644
--- a/basin3d/core/plugin.py
+++ b/basin3d/core/plugin.py
@@ -17,7 +17,7 @@
 from typing import Dict
 
 from basin3d.core import monitor
-from basin3d.core.catalog import CatalogTinyDb
+from basin3d.core.catalog import CatalogSqlAlchemy
 from basin3d.core.models import DataSource
 from basin3d.core.schema.enum import FeatureTypeEnum
 
@@ -48,7 +48,7 @@ class DataSourcePluginAccess:
     Metaclass for DataSource plugin views.  The should be registered in a subclass of
     :class:`basin3d.plugins.DataSourcePluginPoint` in attribute `plugin_access_classes`.
     """
-    def __init__(self, datasource: DataSource, catalog: CatalogTinyDb):
+    def __init__(self, datasource: DataSource, catalog: CatalogSqlAlchemy):
         """
 
         :param datasource: The data source object
@@ -125,7 +125,7 @@ class DataSourcePluginPoint:
     Base class for DataSourcePlugins.
     """
 
-    def __init__(self, catalog: CatalogTinyDb):
+    def __init__(self, catalog: CatalogSqlAlchemy):
         """
         Instantiate the plugin with the catalog
         :param catalog:
diff --git a/basin3d/core/sqlalchemy_models.py b/basin3d/core/sqlalchemy_models.py
new file mode 100644
index 0000000..f686924
--- /dev/null
+++ b/basin3d/core/sqlalchemy_models.py
@@ -0,0 +1,138 @@
+"""
+..currentmodule:: basin3d.core.sqlalchemy_models
+
+:platform: Unix, Mac
+:synopsis: SQLAlchemy models for BASIN-3D data sources
+:module author: Valerie C. Hendrix <vchendrix@lbl.gov>
+
+.. contents:: Contents
+    :local:
+    :backlinks: top
+
+"""
+from typing import Any
+
+from sqlalchemy import Column, Integer, String, ForeignKey, JSON, UniqueConstraint, Index
+from sqlalchemy import create_engine
+from sqlalchemy.types import Text
+from sqlalchemy.orm import relationship, sessionmaker, declarative_base
+
+Base: Any = declarative_base()
+"""The base class for all SQLAlchemy models"""
+
+
+class DataSource(Base):
+    """
+    Data source model for BASIN-3D data sources
+
+    Fields:
+        - *id:* autoincrement integer, primary key
+        - *plugin_id:* string, unique identifier for the data source
+        - *name:* string, unique name for the data source
+        - *id_prefix:* string, prefix that is added to all data source ids
+        - *location:* string, location of the data source (e.g., URL)
+        - *plugin_module:* string, module name of the data source plugin
+        - *plugin_class:* string, class name of the data source plugin
+    """
+    __tablename__ = 'data_source'
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    plugin_id = Column(String(50), unique=True, nullable=False)
+    name = Column(String(20), unique=True, nullable=False)
+    id_prefix = Column(String(5), unique=True, nullable=False)
+    location = Column(Text, nullable=True)
+    plugin_module = Column(Text, nullable=True)
+    plugin_class = Column(Text, nullable=True)
+
+    __table_args__ = (
+        Index('idx_id_prefix', 'id_prefix'),  # Adding an index on id_prefix
+    )
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self.name
+
+    def __repr__(self):
+        return '<DataSource %r>' % self.name
+
+
+class ObservedProperty(Base):
+    """
+    Observed property model for BASIN-3D data sources
+
+    Fields:
+        - *basin3d_vocab:* string, unique identifier for the observed property
+        - *full_name:* string, full name of the observed property
+        - *categories:* string, delimited string of categories
+        - *units:* string, units of the observed property
+    """
+    __tablename__ = 'observed_property'
+    basin3d_vocab = Column(String(50), primary_key=True, nullable=False)
+    full_name = Column(String(255), nullable=False)
+    categories = Column(Text, nullable=True)  # Assuming categories are stored as a delimited string
+    units = Column(String(50), nullable=False)
+
+    __table_args__ = (
+        Index('idx_basin3d_vocab', 'basin3d_vocab'),  # Adding an index on id_prefix
+    )
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self.description
+
+    def __repr__(self):
+        return '<ObservedProperty %r>' % self.basin3d_vocab
+
+
+class AttributeMapping(Base):
+    """
+    Table to map attributes between BASIN-3D and data sources
+
+    Fields:
+        - *attr_type:* string, type of attribute
+        - *basin3d_vocab:* string, unique identifier for the attribute
+        - *basin3d_desc:* JSON, description of the attribute
+        - *datasource_vocab:* string, unique identifier for the data source attribute
+        - *datasource_desc:* text, description of the data source attribute
+        - *datasource_id:* integer, foreign key to the data source
+
+    """
+    __tablename__ = 'attribute_mapping'
+    id = Column(Integer, primary_key=True, autoincrement=True)
+    attr_type = Column(String(50), nullable=False)
+    basin3d_vocab = Column(String(50), nullable=False)
+    basin3d_desc = Column(JSON, nullable=False)
+    datasource_vocab = Column(String(50), nullable=False)
+    datasource_desc = Column(Text, nullable=True)
+    datasource_id = Column(Integer, ForeignKey('data_source.id'), nullable=False)
+    datasource = relationship('DataSource')
+
+    __table_args__ = (
+        UniqueConstraint('datasource_id', 'attr_type', 'datasource_vocab', name='_datasource_attr_type_vocab_uc'),
+        Index('idx_datasource_attr_type', 'datasource_id', 'attr_type', 'datasource_vocab'),
+        # Adding an index on id_prefix
+    )
+
+    def __str__(self):
+        return self.__unicode__()
+
+    def __unicode__(self):
+        return self.datasource_vocab
+
+
+def clear_database():
+    """
+    Clear the database
+    """
+    Base.metadata.drop_all(engine)
+    Base.metadata.create_all(engine)
+
+
+# Database setup in memory
+# Create a temporary in memory SQLite database
+engine = create_engine('sqlite:///:memory:')
+Base.metadata.create_all(engine)
+Session = sessionmaker(bind=engine)
diff --git a/basin3d/synthesis.py b/basin3d/synthesis.py
index 77ebf28..1ed91a5 100644
--- a/basin3d/synthesis.py
+++ b/basin3d/synthesis.py
@@ -28,7 +28,7 @@
 from importlib import import_module
 from typing import List, Union, cast
 
-from basin3d.core.catalog import CatalogTinyDb
+from basin3d.core.catalog import CatalogSqlAlchemy
 from basin3d.core.models import DataSource
 from basin3d.core.plugin import PluginMount
 from basin3d.core.schema.query import QueryMeasurementTimeseriesTVP, QueryMonitoringFeature, SynthesisResponse
@@ -63,7 +63,7 @@ def register(plugins: List[str] = None):
         raise SynthesisException("There are no plugins to register")
 
     plugin_dict = {}
-    catalog = CatalogTinyDb()
+    catalog = CatalogSqlAlchemy()
     for plugin in plugins:
         if isinstance(plugin, str):
             # If this is a string  convert to module and class then load
@@ -90,7 +90,7 @@ class DataSynthesizer:
     Synthesis API
     """
 
-    def __init__(self, plugins: dict, catalog: CatalogTinyDb):
+    def __init__(self, plugins: dict, catalog: CatalogSqlAlchemy):
         self._plugins = plugins
         self._catalog = catalog
         self._datasources = {}
@@ -155,17 +155,19 @@ def attribute_mappings(self, datasource_id=None, attr_type=None, attr_vocab=None
         >>> response = synthesizer.attribute_mappings(datasource_id='USGS', attr_type='STATISTIC')
         >>> for attr_mapping in response:
         ...     print(f'{attr_mapping.attr_type} | {attr_mapping.basin3d_vocab} -- {attr_mapping.datasource_vocab}')
-        STATISTIC | MEAN -- 00003
-        STATISTIC | MIN -- 00002
         STATISTIC | MAX -- 00001
+        STATISTIC | MIN -- 00002
+        STATISTIC | MEAN -- 00003
         STATISTIC | TOTAL -- 00006
 
+
         >>> response = synthesizer.attribute_mappings(datasource_id='USGS', attr_type='RESULT_QUALITY', attr_vocab=['VALIDATED', 'ESTIMATED'], from_basin3d=True)
         >>> for attr_mapping in response:
         ...     print(f'{attr_mapping.attr_type} | {attr_mapping.basin3d_vocab} -- {attr_mapping.datasource_vocab}, {attr_mapping.datasource_desc}')
-        RESULT_QUALITY | ESTIMATED -- e, Value has been edited or estimated by USGS personnel and is write protected
-        RESULT_QUALITY | ESTIMATED -- E, Value was computed from estimated unit values.
         RESULT_QUALITY | VALIDATED -- A, Approved for publication -- Processing and review completed.
+        RESULT_QUALITY | ESTIMATED -- E, Value was computed from estimated unit values.
+        RESULT_QUALITY | ESTIMATED -- e, Value has been edited or estimated by USGS personnel and is write protected
+
 
         Return all the :class:`basin3d.core.models.AttributMapping` registered or those that match the specified fields.
 
diff --git a/docs/quick_guide.rst b/docs/quick_guide.rst
index ce98856..1269f67 100644
--- a/docs/quick_guide.rst
+++ b/docs/quick_guide.rst
@@ -74,7 +74,7 @@ Once logging is configured, all BASIN-3D logging is outputted.
 
    >>> synthesizer = synthesis.register()
    2022-04-14T16:59:31.082 INFO * basin3d.core.synthesis * - Loading Plugin = USGSDataSourcePlugin
-   2022-04-14T16:59:31.083 DEBUG * basin3d.core.catalog * - Initializing CatalogTinyDb metadata catalog
+   2022-04-14T16:59:31.083 DEBUG * basin3d.core.catalog * - Initializing CatalogSqlAlchemy metadata catalog
    2022-04-14T16:59:31.084 INFO * basin3d.core.catalog * - Loading metadata catalog for Plugin USGS
    2022-04-14T16:59:31.085 DEBUG * basin3d.core.catalog * - Mapping file mapping_usgs.csv for plugin package basin3d.plugins
    2022-04-14T16:59:31.086 DEBUG * basin3d.core.catalog * - Mapped 00400 to pH
@@ -114,4 +114,4 @@ Write log messages to a file called `basin3d.log`:
     >>> synthesizer = synthesis.register()
     2022-04-14T17:32:50.502 INFO * basin3d.core.synthesis * - Loading Plugin = USGSDataSourcePlugin
     2022-04-14T17:32:50.507 INFO * basin3d.core.catalog * - Loading metadata catalog for Plugin USGS
-    2022-04-14T17:32:50.509 INFO * basin3d.core.catalog * - Initialized CatalogTinyDb metadata catalog
+    2022-04-14T17:32:50.509 INFO * basin3d.core.catalog * - Initialized CatalogSqlAlchemy metadata catalog
diff --git a/pyproject.toml b/pyproject.toml
index 00353ec..e1c093f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,7 +40,7 @@ dependencies = [
     "pydantic<2",
     "pyyaml>=6.0",
     "requests",
-    "tinydb",
+    'SQLAlchemy>=1.4',
 ]
 
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index d396888..617fd3c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -59,8 +59,8 @@ def plugin_access_alpha():
     from basin3d.core.models import MeasurementTimeseriesTVPObservation
 
     from tests.testplugins import alpha
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     plugins = [alpha.AlphaSourcePlugin(catalog)]
     catalog.initialize(plugins)
     return plugins[0].access_classes[MeasurementTimeseriesTVPObservation]
@@ -74,8 +74,8 @@ def plugin_access():
     from basin3d.plugins.usgs import USGSMeasurementTimeseriesTVPObservationAccess
 
     from basin3d.plugins import usgs
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     plugins = [usgs.USGSDataSourcePlugin(catalog)]
     catalog.initialize(plugins)
     return USGSMeasurementTimeseriesTVPObservationAccess(plugins[0].datasource, catalog)
diff --git a/tests/test_core_catalog.py b/tests/test_core_catalog.py
index ccb1aaa..359d11b 100644
--- a/tests/test_core_catalog.py
+++ b/tests/test_core_catalog.py
@@ -2,16 +2,30 @@
 from unittest.mock import Mock
 
 import pytest
-from tinydb import TinyDB
 
-from basin3d.core.catalog import CatalogException
-from basin3d.core.models import DataSource, ObservedProperty, AttributeMapping
+import basin3d.core.models
+from basin3d.core.catalog import CatalogException, DATASOURCE_NONE
 from basin3d.core.schema.enum import SamplingMediumEnum, StatisticEnum
 from tests.testplugins import alpha, complexmap
 from basin3d.plugins import usgs
 
 log = logging.Logger(__name__)
 
+# ---------------------------------------
+# Helper comparison functions
+
+
+def compare_models(instance1, instance2):
+    """
+    Compare two SQLAlchemy model instances and return the differences.
+    """
+    differences = {}
+    for column in instance1.__table__.columns:
+        value1 = getattr(instance1, column.name)
+        value2 = getattr(instance2, column.name)
+        if value1 != value2:
+            differences[column.name] = (value1, value2)
+    return differences
 
 # ---------------------------------------
 # Helper and mock functions
@@ -33,11 +47,6 @@ def mock_init_catalog(dummyself):
     return test_catalog
 
 
-def get_datasource_from_id(id, id_prefix, location='test.location'):
-    """Helper file to turn id into datasource"""
-    return DataSource(id=id, name=id, id_prefix=id_prefix, location=location)
-
-
 # ---------------------------------------
 # Fixtures
 
@@ -49,14 +58,18 @@ def test_catalog():
 
 @pytest.fixture
 def catalog(monkeypatch):
-    from basin3d.core.catalog import CatalogTinyDb
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    from basin3d.core.sqlalchemy_models import clear_database
 
-    catalog = CatalogTinyDb()
+    catalog = CatalogSqlAlchemy()
     catalog.plugin_dir = 'tests.testplugins'
 
     catalog.initialize([])
 
-    return catalog
+    yield catalog
+
+    clear_database()
+
 
 
 # ---------------------------------------
@@ -72,11 +85,11 @@ def test_get_attribute_enum():
     assert catalog._get_attribute_enum('FOO', SamplingMediumEnum) is None
 
 
-@pytest.mark.parametrize("plugins", [(["foo"]), [Mock()]])
+@pytest.mark.parametrize("plugins", [(["foo"])])
 def test_catalog_error(plugins):
     """Test Catalog Error"""
-    from basin3d.core.catalog import CatalogTinyDb, CatalogException
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy, CatalogException
+    catalog = CatalogSqlAlchemy()
     pytest.raises(CatalogException, catalog.initialize, plugins)
 
 
@@ -89,7 +102,7 @@ def test_gen_basin3d_variable_store(catalog, caplog):
     caplog.clear()
     catalog.variable_filename = 'basin3d_variables_duplicate.csv'
     catalog._gen_variable_store()
-    assert catalog._get_observed_property('Br').to_dict() == ObservedProperty(
+    assert catalog._get_observed_property('Br').to_dict() == basin3d.core.models.ObservedProperty(
         basin3d_vocab='Br', full_name='Bromide (Br)',
         categories=['Biogeochemistry', 'Anions'], units='mM').to_dict()
     log_msgs = [rec.message for rec in caplog.records]
@@ -104,7 +117,7 @@ def test_gen_basin3d_variable_store(catalog, caplog):
     catalog.variable_filename = 'basin3d_observed_property_vocabulary.csv'
     catalog.variable_dir = 'basin3d.data'
     catalog._gen_variable_store()
-    assert catalog._get_observed_property('ACT') == ObservedProperty(
+    assert catalog._get_observed_property('ACT') == basin3d.core.models.ObservedProperty(
         basin3d_vocab='ACT', full_name='Acetate (CH3COO)',
         categories=['Biogeochemistry', 'Anions'], units='mM')
     assert catalog._get_observed_property('heya') is None
@@ -112,16 +125,24 @@ def test_gen_basin3d_variable_store(catalog, caplog):
 
 # catalog.find_observed_property (TinyDB)
 def test_find_observed_property(catalog):
-    assert catalog.find_observed_property('ACT') == ObservedProperty(basin3d_vocab='ACT',
+    assert catalog.find_observed_property('ACT') == basin3d.core.models.ObservedProperty(basin3d_vocab='ACT',
                                                                      full_name='Acetate (CH3COO)',
                                                                      categories=['Biogeochemistry' ,'Anions'],
                                                                      units='mM')
     assert catalog.find_observed_property('FOO') is None
 
 
-def test_not_initialized_errors(caplog):
-    from basin3d.core.catalog import CatalogTinyDb, CatalogException
-    new_catalog = CatalogTinyDb()
+def test_not_initialized_errors(caplog, monkeypatch):
+    """
+    Test the errors when the catalog is not initialized
+
+    :param caplog: The caplog fixture
+    :param monkeypatch: The monkeypatch fixture
+    """
+    from basin3d.core.catalog import CatalogSqlAlchemy, CatalogException
+    new_catalog = CatalogSqlAlchemy()
+
+    monkeypatch.setattr(new_catalog, 'is_initialized', Mock(return_value=False))
 
     caplog.clear()
     with pytest.raises(CatalogException):
@@ -175,8 +196,17 @@ def test_find_observed_properties(caplog, catalog):
     """Test the observed property search works"""
     caplog.set_level(logging.INFO)
 
+    # specified valid observed properties returned
+    assert ['ACT', 'Br'] == [i.basin3d_vocab for i in catalog.find_observed_properties(['ACT', 'Br'])]
+
+    # on invalid observed properties is not returned and warning message generated.
+    caplog.clear()
+    assert ['ACT', 'Br'] == [i.basin3d_vocab for i in catalog.find_observed_properties(['ACT', 'FOO', 'Br'])]
+    log_msgs = [rec.message for rec in caplog.records]
+    assert 'BASIN-3D does not support variable FOO' in log_msgs
+
     # all observed_properties are returned
-    assert ['ACT', 'Br', 'Cl', 'DIN', 'DTN', 'F', 'NO3', 'NO2', 'PO4', 'SO4', 'S2', 'S2O3', 'HCO3', 'DIC', 'DOC',
+    assert [i.basin3d_vocab for i in catalog.find_observed_properties()] == ['ACT', 'Br', 'Cl', 'DIN', 'DTN', 'F', 'NO3', 'NO2', 'PO4', 'SO4', 'S2', 'S2O3', 'HCO3', 'DIC', 'DOC',
             'NPOC', 'POC', 'TIC', 'TOC', 'd13C', 'CO2_d13C_soilgas', 'CH4_d13C_soilgas', 'd15N', 'N2O_d15N_soilgas',
             'CO2_d18O_soilgas', 'N2O_d18O_soilgas', 'SO4_d34S', 'U235', 'H2O_d18O', 'H2O_dD', 'NH4', 'Ca', 'Mg', 'K',
             'Si', 'Na', 'MICRO_COV', 'MGO1', 'MGO2', 'MGO3', 'GENE_TRAN', 'MICRO_PEP', 'MICRO_PLFA', 'NH3', 'CO2', 'H2',
@@ -188,18 +218,7 @@ def test_find_observed_properties(caplog, catalog):
             'W_SPD', 'W_CH', 'ERT', 'CEC', 'EXAFS', 'SEQ', 'XRD', 'XRF', 'XANES', 'HCND', 'SED_SIZE', 'HCND_Sat',
             'Porosity', 'SBD', 'SYD', 'HCND_Unsat', 'RET_CUR', 'LSE', 'GWF', 'Well logs', 'SAT', 'SDE', 'SMO', 'STM',
             'SWP', 'SEC', 'FDOM', 'DO', 'EC', 'SC', 'SAL', 'GWL', 'PH', 'ORP', 'RDC', 'SWL', 'WLH', 'WLE', 'WT', 'TDS',
-            'TSS', 'TRB', 'STO_RES', 'LAI', 'PLT_HT', 'PAI', 'PFT', 'RGB', 'GCC', 'NDVI'] == [i.basin3d_vocab for
-                                                                                              i in
-                                                                                              catalog.find_observed_properties()]
-
-    # specified valid observed properties returned
-    assert ['ACT', 'Br'] == [i.basin3d_vocab for i in catalog.find_observed_properties(['ACT', 'Br'])]
-
-    # on invalid observed properties is not returned and warning message generated.
-    caplog.clear()
-    assert ['ACT', 'Br'] == [i.basin3d_vocab for i in catalog.find_observed_properties(['ACT', 'FOO', 'Br'])]
-    log_msgs = [rec.message for rec in caplog.records]
-    assert 'BASIN-3D does not support variable FOO' in log_msgs
+            'TSS', 'TRB', 'STO_RES', 'LAI', 'PLT_HT', 'PAI', 'PFT', 'RGB', 'GCC', 'NDVI']
 
 
 def test_process_plugin_attr_mapping(catalog, caplog):
@@ -208,12 +227,12 @@ def test_process_plugin_attr_mapping(catalog, caplog):
     from basin3d.core.catalog import CatalogException
     with pytest.raises(CatalogException):
         catalog._process_plugin_attr_mapping(
-            alpha.AlphaSourcePlugin(catalog), filename='mapping_alpha_wrong_header.csv', datasource=DataSource())
+            alpha.AlphaSourcePlugin(catalog), filename='mapping_alpha_wrong_header.csv', datasource=basin3d.core.models.DataSource())
 
     caplog.set_level(logging.INFO)
     caplog.clear()
     from tests.testplugins import beta
-    catalog._process_plugin_attr_mapping(beta.BetaSourcePlugin(catalog), filename='beta_mapping.csv', datasource=DataSource(id='Beta'))
+    catalog._process_plugin_attr_mapping(beta.BetaSourcePlugin(catalog), filename='beta_mapping.csv', datasource=basin3d.core.models.DataSource(id='Beta'))
     log_msgs = [rec.message for rec in caplog.records]
     assert 'Datasource Beta: Attribute type STAT is not supported. Skipping mapping.' in log_msgs
     assert 'Datasource Beta: basin3d_vocab MAXIMUM for attr_type STATISTIC is not a valid BASIN-3D vocabulary. Skipping attribute mapping.' in log_msgs
@@ -223,65 +242,65 @@ def test_process_plugin_attr_mapping(catalog, caplog):
                          # Wrong-plugin-inititalized
                          [([usgs.USGSDataSourcePlugin],
                            {'datasource_id': 'Alpha', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'ACT'},
-                           AttributeMapping(
+                           basin3d.core.models.AttributeMapping(
                                attr_type='OBSERVED_PROPERTY',
                                basin3d_vocab='NOT_SUPPORTED',
                                basin3d_desc=[],
                                datasource_vocab='ACT',
                                datasource_desc='No datasource was found for id "Alpha".',
-                               datasource=DataSource())
+                               datasource=DATASOURCE_NONE)
                            ),
                           # USGS
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
                            {'datasource_id': 'USGS', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': '00095'},
-                           AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+                           basin3d.core.models.AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
                                             basin3d_vocab='SC:WATER',
-                                            basin3d_desc=[ObservedProperty(basin3d_vocab='SC', full_name='Specific Conductance (SC)',
+                                            basin3d_desc=[basin3d.core.models.ObservedProperty(basin3d_vocab='SC', full_name='Specific Conductance (SC)',
                                                                            categories=['Hydrogeology', 'Water Physical/Quality Parameters'], units='uS/cm'),
                                                           SamplingMediumEnum.WATER],
                                             datasource_vocab='00095',
                                             datasource_desc='Specific conductance, water, unfiltered, microsiemens per centimeter at 25 degrees Celsius',
-                                            datasource=DataSource(id='USGS', name='USGS', id_prefix='USGS',
-                                                                  location='https://waterservices.usgs.gov/nwis/', credentials={}))
+                                            datasource=basin3d.core.models.DataSource(id='USGS', name='USGS', id_prefix='USGS',
+                                                                  location='https://waterservices.usgs.gov/nwis/'))
                            ),
                           # Alpha
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
                            {'datasource_id': 'Alpha', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'Acetate'},
-                           AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+                           basin3d.core.models.AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
                                             basin3d_vocab='ACT:WATER',
-                                            basin3d_desc=[ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM'),
+                                            basin3d_desc=[basin3d.core.models.ObservedProperty(basin3d_vocab='ACT', full_name='Acetate (CH3COO)', categories=['Biogeochemistry', 'Anions'], units='mM'),
                                                           SamplingMediumEnum.WATER],
                                             datasource_vocab='Acetate',
                                             datasource_desc='acetate',
-                                            datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))
+                                            datasource=basin3d.core.models.DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))
                            ),
                           # Complexmap
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin, complexmap.ComplexmapSourcePlugin],
                            {'datasource_id': 'Complexmap', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'Mean Al'},
-                           AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC',
+                           basin3d.core.models.AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM:STATISTIC',
                                             basin3d_vocab='Al:WATER:MEAN',
-                                            basin3d_desc=[ObservedProperty(basin3d_vocab='Al', full_name='Aluminum (Al)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
+                                            basin3d_desc=[basin3d.core.models.ObservedProperty(basin3d_vocab='Al', full_name='Aluminum (Al)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
                                                           SamplingMediumEnum.WATER, StatisticEnum.MEAN],
                                             datasource_vocab='Mean Al',
                                             datasource_desc='aluminum (Al) concentration in water',
-                                            datasource=DataSource(id='Complexmap', name='Complexmap', id_prefix='C', location='https://asource.foo/', credentials={}))
+                                            datasource=basin3d.core.models.DataSource(id='Complexmap', name='Complexmap', id_prefix='C', location='https://asource.foo/', credentials={}))
                            ),
                           # Bad-DataSource
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
                            {'datasource_id': 'FOO', 'attr_type': 'OBSERVED_PROPERTY', 'datasource_vocab': 'Acetate'},
-                           AttributeMapping(attr_type='OBSERVED_PROPERTY',
+                           basin3d.core.models.AttributeMapping(attr_type='OBSERVED_PROPERTY',
                                             basin3d_vocab='NOT_SUPPORTED',
                                             basin3d_desc=[],
                                             datasource_vocab='Acetate',
                                             datasource_desc='No datasource was found for id "FOO".',
-                                            datasource=DataSource())
+                                            datasource=DATASOURCE_NONE)
                            )
                           ],
                          ids=['Wrong-plugin-initialized', 'USGS', 'Alpha', 'Complexmap', 'Bad-DataSource'])
 def test_find_datasource_attribute_mapping(plugins, query, expected):
     """ Test attribute mapping """
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in plugins])
 
     attribute_mapping = catalog.find_datasource_attribute_mapping(**query)
@@ -297,19 +316,19 @@ def test_find_datasource_attribute_mapping(plugins, query, expected):
 # -- datasource vocab to BASIN-3D
 #    -- single datasource vocab (with attr_type specified) with expected single BASIN-3D
 # -- all mappings: to find complex mappings
-# catalog.find_attribute_mappings (TinyDB)
+# catalog.find_attribute_mappings (SqlAlchemy)
 @pytest.mark.parametrize('plugins, query, expected_count, expected_list, expected_msg',
                          # ds_id-attr_type-attr_vocab-from_basin3d--USGS-OBSERVED_PROPERTY-Hg--compound
                          [
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin],
                            {'datasource_id': 'USGS', 'attr_type': 'OBSERVED_PROPERTY', 'attr_vocab': 'Hg', 'from_basin3d': True}, 1,
-                           [AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
+                           [basin3d.core.models.AttributeMapping(attr_type='OBSERVED_PROPERTY:SAMPLING_MEDIUM',
                                              basin3d_vocab='Hg:WATER',
-                                             basin3d_desc=[ObservedProperty(basin3d_vocab='Hg', full_name='Mercury (Hg)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
+                                             basin3d_desc=[basin3d.core.models.ObservedProperty(basin3d_vocab='Hg', full_name='Mercury (Hg)', categories=['Biogeochemistry', 'Trace elements'], units='mg/L'),
                                                            SamplingMediumEnum.WATER],
                                              datasource_vocab='50287',
                                              datasource_desc='Mercury, water, filtered, nanograms per liter',
-                                             datasource=DataSource(id='USGS', name='USGS', id_prefix='USGS', location='https://waterservices.usgs.gov/nwis/', credentials={}))],
+                                             datasource=basin3d.core.models.DataSource(id='USGS', name='USGS', id_prefix='USGS', location='https://waterservices.usgs.gov/nwis/'))],
                            []),
                           # datasource_id-only-Alpha-all
                           ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha'}, 14, [], []),
@@ -323,30 +342,30 @@ def test_find_datasource_attribute_mapping(plugins, query, expected):
                           ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_type': 'STATISTIC', 'attr_vocab': 'MEAN', 'from_basin3d': True}, 1, [], []),
                           # ds_id-attr_vocab--Alpha-mean
                           ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_vocab': 'mean'}, 1,
-                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                           [basin3d.core.models.AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
                                              datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
-                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))],
+                                             datasource=basin3d.core.models.DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/'))],
                            []),
                           # ds_id-attr_vocab-from_basin3d--Alpha-MEAN
                           ([alpha.AlphaSourcePlugin], {'datasource_id': 'Alpha', 'attr_vocab': 'MEAN', 'from_basin3d': True}, 1,
-                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                           [basin3d.core.models.AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
                                              datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
-                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))],
+                                             datasource=basin3d.core.models.DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/'))],
                            []),
                           # attr_type_attr_vocab--STATISTIC-mean
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_type': 'STATISTIC', 'attr_vocab': 'mean'}, 1,
-                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                           [basin3d.core.models.AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
                                              datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
-                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={}))],
+                                             datasource=basin3d.core.models.DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/'))],
                            []),
                           # attr_type_attr_vocab-from_basin3d--STATISTIC-MEAN
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_type': 'STATISTIC', 'attr_vocab': 'MEAN', 'from_basin3d': True}, 2,
-                           [AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
+                           [basin3d.core.models.AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='mean',
                                              datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
-                                             datasource=DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/', credentials={})),
-                            AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='00003',
+                                             datasource=basin3d.core.models.DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')),
+                            basin3d.core.models.AttributeMapping(attr_type='STATISTIC', basin3d_vocab='MEAN', datasource_vocab='00003',
                                              datasource_desc='', basin3d_desc=[StatisticEnum.MEAN],
-                                             datasource=DataSource(id='USGS', name='USGS', id_prefix='USGS', location='https://waterservices.usgs.gov/nwis/', credentials={}))],
+                                             datasource=basin3d.core.models.DataSource(id='USGS', name='USGS', id_prefix='USGS', location='https://waterservices.usgs.gov/nwis/'))],
                            []),
                           # ds_id-attr_type_attr_vocab-from_basin3d-similar-vocab--USGS-RESULT_QUALITY
                           ([usgs.USGSDataSourcePlugin], {'datasource_id': 'USGS', 'attr_type': 'RESULT_QUALITY', 'attr_vocab': ['ESTIMATED', 'VALIDATED'], 'from_basin3d': True}, 3, [], []),
@@ -362,7 +381,7 @@ def test_find_datasource_attribute_mapping(plugins, query, expected):
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': ['Al', 'ACT'], 'from_basin3d': True}, 4, [], []),
                           # attr_vocabs-some_bad--Aluminum,Acetate,Foo
                           ([usgs.USGSDataSourcePlugin, alpha.AlphaSourcePlugin], {'attr_vocab': ['Aluminum', 'Acetate', 'Foo']}, 2, [],
-                           ['No attribute mappings found for the following datasource vocabularies: Foo. Note: specified datasource id = None and attribute type = None']),
+                           ['No attribute mappings found for the following datasource vocabularies: Foo. Note: specified datasource id = None and attribute type = None.']),
                           # BAD-ds_id
                           ([alpha.AlphaSourcePlugin], {'datasource_id': 'Foo'}, -1, [], []),
                           # BAD-attr_type
@@ -400,29 +419,31 @@ def test_find_attribute_mappings(caplog, plugins, query, expected_count, expecte
     """ Test attribute mapping """
     caplog.set_level(logging.INFO)
 
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in plugins])
     caplog.clear()
 
-    attribute_mappings = catalog.find_attribute_mappings(**query)
+    # Check the results, count, and messages
+    with caplog.at_level(logging.INFO):
+        attribute_mappings = catalog.find_attribute_mappings(**query)
 
-    actual_list = []
-    if expected_count >= 0:
-        count = 0
-        for attr_mapping in attribute_mappings:
-            count += 1
-            if expected_list:
-                actual_list.append(attr_mapping)
-        assert count == expected_count
-        if expected_list:
-            for expected_attr_mapping in expected_list:
-                assert expected_attr_mapping in actual_list
-        if expected_msg:
-            log_msgs = [rec.message for rec in caplog.records]
-            for msg in expected_msg:
-                assert msg in log_msgs
-    else:
-        with pytest.raises(CatalogException):
+        actual_list = []
+        if expected_count >= 0:
+            count = 0
             for attr_mapping in attribute_mappings:
-                pass
+                count += 1
+                if expected_list:
+                    actual_list.append(attr_mapping)
+            assert count == expected_count
+            if expected_list:
+                for expected_attr_mapping in expected_list:
+                    assert expected_attr_mapping in actual_list
+            if expected_msg:
+                log_msgs = [rec.message for rec in caplog.records]
+                for msg in expected_msg:
+                    assert msg in log_msgs
+        else:
+            with pytest.raises(CatalogException):
+                for attr_mapping in attribute_mappings:
+                    pass
diff --git a/tests/test_core_models.py b/tests/test_core_models.py
index 1d415a5..7b08853 100644
--- a/tests/test_core_models.py
+++ b/tests/test_core_models.py
@@ -80,7 +80,7 @@ def mapped_attribute_agg_dur_not_supported(datasource):
                                basin3d_vocab='NOT_SUPPORTED',
                                basin3d_desc=[],
                                datasource_vocab='daily',
-                               datasource_desc='No mapping was found for datasource vocab: "daily" in datasource: "Alpha".',
+                               datasource_desc='No mapping was found for attr: "AGGREGATION_DURATION" and for datasource vocab: "daily" in datasource: "Alpha".',
                                datasource=datasource))
 
 
@@ -140,7 +140,8 @@ def test_mapped_attribute_not_supported(mapped_attribute_agg_dur_not_supported,
     assert attr_mapping.basin3d_vocab == 'NOT_SUPPORTED'
     assert attr_mapping.basin3d_desc == []
     assert attr_mapping.datasource_vocab == 'daily'
-    assert attr_mapping.datasource_desc == 'No mapping was found for datasource vocab: "daily" in datasource: "Alpha".'
+    assert attr_mapping.datasource_desc == 'No mapping was found for attr: "AGGREGATION_DURATION" and for ' \
+                                           'datasource vocab: "daily" in datasource: "Alpha".'
     assert attr_mapping.datasource == datasource
 
 
diff --git a/tests/test_core_plugins.py b/tests/test_core_plugins.py
index 4486438..d12321c 100644
--- a/tests/test_core_plugins.py
+++ b/tests/test_core_plugins.py
@@ -3,7 +3,7 @@
 import pytest
 
 import basin3d
-from basin3d.core.catalog import CatalogTinyDb
+from basin3d.core.catalog import CatalogSqlAlchemy
 from basin3d.core.models import MonitoringFeature
 from basin3d.core.plugin import get_feature_type
 from basin3d.core.schema.enum import FeatureTypeEnum
@@ -28,8 +28,8 @@ def test_plugin_attribs():
 def test_plugin_access_objects():
     """Test the plugin and check the access objects available"""
     from tests.testplugins import alpha
-    assert isinstance(alpha.AlphaSourcePlugin(CatalogTinyDb()).get_plugin_access(), dict)
-    plugin_access_objects = alpha.AlphaSourcePlugin(CatalogTinyDb()).get_plugin_access()
+    assert isinstance(alpha.AlphaSourcePlugin(CatalogSqlAlchemy()).get_plugin_access(), dict)
+    plugin_access_objects = alpha.AlphaSourcePlugin(CatalogSqlAlchemy()).get_plugin_access()
     assert list(plugin_access_objects.keys()) == [
         basin3d.core.models.MeasurementTimeseriesTVPObservation,
         basin3d.core.models.MonitoringFeature]
@@ -69,7 +69,7 @@ def test_plugin_views(monkeypatch):
     from tests.testplugins import alpha
 
     monkeypatch.setattr(alpha.AlphaSourcePlugin, 'get_datasource', Mock())
-    plugin_views = alpha.AlphaSourcePlugin(CatalogTinyDb()).get_plugin_access()
+    plugin_views = alpha.AlphaSourcePlugin(CatalogSqlAlchemy()).get_plugin_access()
     assert isinstance(plugin_views, dict)
     assert basin3d.core.models.MeasurementTimeseriesTVPObservation in plugin_views
     assert basin3d.core.models.MonitoringFeature in plugin_views
diff --git a/tests/test_core_synthesis.py b/tests/test_core_synthesis.py
index d05d4da..0ab9489 100644
--- a/tests/test_core_synthesis.py
+++ b/tests/test_core_synthesis.py
@@ -10,8 +10,8 @@
 
 @pytest.fixture
 def alpha_plugin_access():
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
     alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
 
@@ -31,8 +31,8 @@ def alpha_plugin_access():
                             'start_date': '2021-01-01'}, ['DAY']),
                           ], ids=['change-to-day', 'NONE', 'not-specified'])
 def test_measurement_timeseries_TVP_observation_access_synthesize_query(input_query, expected_result, alpha_plugin_access):
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
     alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
     alpha_access = MeasurementTimeseriesTVPObservationAccess(alpha_ds, catalog)
@@ -45,8 +45,8 @@ def test_measurement_timeseries_TVP_observation_access_synthesize_query(input_qu
 
 
 def test_monitoring_feature_retrieve_no_id():
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
     alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
     # cheating and assigning a datasource to the plugin field as it won't be used
diff --git a/tests/test_core_translate.py b/tests/test_core_translate.py
index e0fe8b6..105d305 100644
--- a/tests/test_core_translate.py
+++ b/tests/test_core_translate.py
@@ -9,20 +9,24 @@
 from tests.testplugins import alpha, complexmap
 
 
-@pytest.fixture
 def alpha_plugin_access():
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    """
+    Create a DataSourcePluginAccess object for the Alpha plugin
+    """
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in [alpha.AlphaSourcePlugin]])
     alpha_ds = DataSource(id='Alpha', name='Alpha', id_prefix='A', location='https://asource.foo/')
 
     return DataSourcePluginAccess(alpha_ds, catalog)
 
 
-@pytest.fixture
 def complex_plugin_access():
-    from basin3d.core.catalog import CatalogTinyDb
-    catalog = CatalogTinyDb()
+    """
+    Create a DataSourcePluginAccess object for the Complexmap plugin
+    """
+    from basin3d.core.catalog import CatalogSqlAlchemy
+    catalog = CatalogSqlAlchemy()
     catalog.initialize([p(catalog) for p in [complexmap.ComplexmapSourcePlugin]])
     complex_ds = DataSource(id='Complexmap', name='Complexmap', id_prefix='C', location='https://asource.foo/')
 
@@ -73,11 +77,11 @@ def complex_plugin_access():
          'compound-compound_query_lists', 'compound-compound_query_no_match',
          'non-compound_non-query-class', 'compound-simple_query_non-query-class',
          'compound-compound_query_non-query-class-lists', 'compound_non-query-class_str-value'])
-def test_find_datasource_vocab(alpha_plugin_access, caplog, attr_type, basin3d_vocab, basin3d_query, expected_results, expected_msgs):
+def test_find_datasource_vocab(caplog, attr_type, basin3d_vocab, basin3d_query, expected_results, expected_msgs):
     caplog.set_level(logging.INFO)
     caplog.clear()
 
-    results = translate._translate_to_datasource_vocab(alpha_plugin_access, attr_type, basin3d_vocab, basin3d_query)
+    results = translate._translate_to_datasource_vocab(alpha_plugin_access(), attr_type, basin3d_vocab, basin3d_query)
     assert sorted(results) == sorted(expected_results)
 
     if expected_msgs:
@@ -116,12 +120,12 @@ def test_find_datasource_vocab(alpha_plugin_access, caplog, attr_type, basin3d_v
                           # multi-mapped
                           ('Alpha',
                            QueryMeasurementTimeseriesTVP(observed_property=['Al'], start_date='2019-01-01', monitoring_feature=['A-3']),
-                           QueryMeasurementTimeseriesTVP(observed_property=['Aluminum', 'Al'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Al', 'Aluminum'], start_date='2019-01-01', monitoring_feature=['A-3']),
                            {'aggregation_duration': ['DAY']}),
                           # multi-mapped-complex
                           ('Alpha',
                            QueryMeasurementTimeseriesTVP(observed_property=['Al'], sampling_medium=['WATER'], start_date='2019-01-01', monitoring_feature=['A-3']),
-                           QueryMeasurementTimeseriesTVP(observed_property=['Aluminum', 'Al'], start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Al', 'Aluminum'], start_date='2019-01-01', monitoring_feature=['A-3']),
                            {'aggregation_duration': ['DAY']}),
                           # complex-single-compound
                           ('Complexmap',
@@ -136,7 +140,7 @@ def test_find_datasource_vocab(alpha_plugin_access, caplog, attr_type, basin3d_v
                           # complex-other-two-compound
                           ('Complexmap',
                            QueryMeasurementTimeseriesTVP(observed_property=['Al'], statistic=['MEAN'], start_date='2019-01-01', monitoring_feature=['A-3']),
-                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Aluminum', 'Mean Al'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Al', 'Mean Aluminum'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
                            {'aggregation_duration': ['DAY']}),
                           # complex-all-compound
                           ('Complexmap',
@@ -151,22 +155,22 @@ def test_find_datasource_vocab(alpha_plugin_access, caplog, attr_type, basin3d_v
                           # complex-all-compound-multiple-values
                           ('Complexmap',
                            QueryMeasurementTimeseriesTVP(observed_property=['Ag', 'Al'], sampling_medium=['WATER', 'GAS'], statistic=['MIN', 'MEAN'], start_date='2019-01-01', monitoring_feature=['A-3']),
-                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Ag', 'Min Ag_gas', 'Minimum Aluminum', 'Mean Aluminum', 'Mean Al'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
+                           QueryMeasurementTimeseriesTVP(observed_property=['Mean Ag', 'Min Ag_gas', 'Minimum Aluminum', 'Mean Al', 'Mean Aluminum'], sampling_medium=None, statistic=None, start_date='2019-01-01', monitoring_feature=['A-3']),
                            {'aggregation_duration': ['DAY']}),
                           ],
                          ids=['single-compound', 'both-compound', 'single-compound+non-compound', 'not_supported', 'not_supported-opv', 'multi-mapped', 'multi-mapped-complex',
                               'complex-single-compound', 'complex-two-compound', 'complex-other-two-compound', 'complex-all-compound', 'complex-all-compound-no-mapping',
                               'complex-all-compound-multiple-values'])
-def test_translator_translate_mapped_query_attrs(alpha_plugin_access, complex_plugin_access, ds, query, expected_results, set_transformed_attr):
-    plugin_access = complex_plugin_access if ds == 'Complexmap' else alpha_plugin_access
+def test_translator_translate_mapped_query_attrs(ds, query, expected_results, set_transformed_attr):
+    plugin_access = ds == "Complexmap" and complex_plugin_access() or alpha_plugin_access()
     results = translate._translate_mapped_query_attrs(plugin_access, query)
     for attr, value in set_transformed_attr.items():
         setattr(expected_results, attr, value)
     assert results == expected_results
 
 
-def test_translator_order_mapped_fields(alpha_plugin_access):
-    ordered_fields = translate._order_mapped_fields(alpha_plugin_access, ['statistic', 'aggregation_duration', 'sampling_medium', 'observed_property'])
+def test_translator_order_mapped_fields():
+    ordered_fields = translate._order_mapped_fields(alpha_plugin_access(), ['statistic', 'aggregation_duration', 'sampling_medium', 'observed_property'])
     assert ordered_fields == ['observed_property', 'sampling_medium', 'statistic', 'aggregation_duration']
 
 
@@ -204,12 +208,12 @@ def test_translator_is_translated_query_valid(query, set_translated_attr, expect
                            {'monitoring_feature': ['9237'], 'parent_feature': ['00000'], 'id': '345aa'}),
                           ],
                          ids=["single", "multiple", "none", "monitoring_feature_query"])
-def test_translator_prefixed_query_attrs(alpha_plugin_access, query, set_translated_attr):
+def test_translator_prefixed_query_attrs(query, set_translated_attr):
     """Filtering of query arguments"""
     translated_query = query.copy()
     for attr, value in set_translated_attr.items():
         setattr(translated_query, attr, value)
-    assert translate._translate_prefixed_query_attrs(alpha_plugin_access, query) == translated_query
+    assert translate._translate_prefixed_query_attrs(alpha_plugin_access(), query) == translated_query
 
 
 @pytest.mark.parametrize('query, set_translated_attr, set_cleaned_query_attr',
diff --git a/tests/test_plugins_epa.py b/tests/test_plugins_epa.py
index cc05dc8..8c9986a 100644
--- a/tests/test_plugins_epa.py
+++ b/tests/test_plugins_epa.py
@@ -223,7 +223,16 @@ def test_epa_monitoring_feature_id(query, resource_file, mf_id, monkeypatch):
                               'filter-test-estimated', 'filter-test-stat-total', 'filter-test-stat-multiple', "filter-multiple",
                               'aggregation-day', 'empty_return'])
 def test_measurement_timeseries_tvp_observations_epa(additional_filters, epa_data_resource, epa_loc_resource, expected_results, monkeypatch):
-    # Test EPA Timeseries data query
+    """
+    Test EPA Timeseries data query
+
+    :param additional_filters: additional query parameters
+    :param epa_data_resource: resource file containing the EPA data
+    :param epa_loc_resource: resource file containing the EPA location data
+    :param expected_results: expected results
+    :param monkeypatch: pytest fixture
+
+    """
 
     def get_csv_dict(dummyvar):
         with open(os.path.join(dirname(__file__), "resources", epa_data_resource)) as data_file:
diff --git a/tests/testplugins/complexmap.py b/tests/testplugins/complexmap.py
index 9736f69..5b4618e 100644
--- a/tests/testplugins/complexmap.py
+++ b/tests/testplugins/complexmap.py
@@ -1,6 +1,7 @@
-from basin3d.core.plugin import DataSourcePluginPoint
+from basin3d.core.plugin import DataSourcePluginPoint, basin3d_plugin
 
 
+@basin3d_plugin
 class ComplexmapSourcePlugin(DataSourcePluginPoint):
     class DataSourceMeta:
         location = 'https://asource.foo/'