Source code for terracotta.drivers.mysql_meta_store
"""drivers/mysql_meta_store.py
MySQL-backed metadata driver. Metadata is stored in a MySQL database.
"""
import functools
from typing import Optional, Mapping, Sequence
import sqlalchemy as sqla
from sqlalchemy.dialects.mysql import TEXT, VARCHAR
from terracotta.drivers.relational_meta_store import RelationalMetaStore
[docs]
class MySQLMetaStore(RelationalMetaStore):
"""A MySQL-backed metadata driver.
Stores metadata and paths to raster files in MySQL.
Requires a running MySQL server.
The MySQL database consists of 4 different tables:
- ``terracotta``: Metadata about the database itself.
- ``key_names``: Contains two columns holding all available keys and their description.
- ``datasets``: Maps key values to physical raster path.
- ``metadata``: Contains actual metadata as separate columns. Indexed via key values.
This driver caches key names.
"""
SQL_DIALECT = "mysql"
SQL_DRIVER = "pymysql"
SQL_TIMEOUT_KEY = "connect_timeout"
_CHARSET = "utf8mb4"
SQLA_STRING = functools.partial(VARCHAR, charset=_CHARSET)
MAX_PRIMARY_KEY_SIZE = 767 // 4 # Max key length for MySQL is at least 767B
DEFAULT_PORT = 3306
def __init__(self, mysql_path: str) -> None:
"""Initialize the MySQLDriver.
This should not be called directly, use :func:`~terracotta.get_driver` instead.
Arguments:
mysql_path: URL to running MySQL server, in the form
``mysql://username:password@hostname/database``
"""
super().__init__(f"{mysql_path}?charset={self._CHARSET}")
self.SQLA_METADATA_TYPE_LOOKUP["text"] = functools.partial(
TEXT, charset=self._CHARSET
)
# raise an exception if database name is invalid
if not self.url.database:
raise ValueError("database must be specified in MySQL path")
if "/" in self.url.database.strip("/"):
raise ValueError("invalid database path")
@classmethod
def _normalize_path(cls, path: str) -> str:
url = cls._parse_path(path)
path = f"{url.drivername}://{url.host}:{url.port or cls.DEFAULT_PORT}/{url.database}"
path = path.rstrip("/")
return path
def _create_database(self) -> None:
engine = sqla.create_engine(
self.url.set(
database=""
), # `.set()` returns a copy with changed parameters
echo=False,
future=True,
)
with engine.connect() as connection:
connection.execute(sqla.text(f"CREATE DATABASE {self.url.database}"))
connection.commit()
def _initialize_database(
self, keys: Sequence[str], key_descriptions: Optional[Mapping[str, str]] = None
) -> None:
# total primary key length has an upper limit in MySQL
self.SQL_KEY_SIZE = self.MAX_PRIMARY_KEY_SIZE // len(keys)
super()._initialize_database(keys, key_descriptions)