Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ bigquery = ["ibis-framework[bigquery]>=11.0.0"]
pyspark = ["setuptools", "ibis-framework[pyspark]>=11.0.0"]
clickhouse = ["ibis-framework[clickhouse]>=11.0.0"]
databricks = ["databricks-sql-connector>=4", "ibis-framework[databricks]>=11.0.0"]
singlestoredb = ["singlestoredb>=1.0", "ibis-framework[singlestoredb]>=11.0.0"]
trino = ["ibis-framework[trino]>=11.0.0"]


Expand Down Expand Up @@ -94,7 +95,3 @@ tests = ["tests", "*/mountainash-data/tests"]
[tool.coverage.report]
exclude_lines = ["no cov", "if __name__ == .__main__:", "if TYPE_CHECKING:"]


[tool.pyright]
venvPath = "/home/nathanielramm/git/mountainash-ide/mountainash-dev-local/.venv"
venv = ".venv"
46 changes: 46 additions & 0 deletions src/mountainash_data/backends/ibis/dialects/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,46 @@ def _build_databricks_connection(**config: t.Any) -> t.Any:
return ibis.databricks.connect(**kwargs)


def _build_singlestoredb_connection(**config: t.Any) -> t.Any:
"""Build a SingleStoreDB ibis connection.

Uses ibis.singlestoredb.connect() with kwargs: host, port, user, password,
database, driver, autocommit, local_infile.
"""
import ibis

host = config.get("host", "localhost")
port = config.get("port", 3306)
user = config.get("user", config.get("username", None))
password = config.get("password", None)
database = config.get("database", None)
driver = config.get("driver", None)
autocommit = config.get("autocommit", True)
local_infile = config.get("local_infile", True)

known = {"host", "port", "user", "username", "password", "database",
"driver", "autocommit", "local_infile", "connection_string"}
extra = {k: v for k, v in config.items() if k not in known}

kwargs: dict[str, t.Any] = {
"host": host,
"port": port,
"autocommit": autocommit,
"local_infile": local_infile,
}
if user is not None:
kwargs["user"] = user
if password is not None:
kwargs["password"] = password
if database is not None:
kwargs["database"] = database
if driver is not None:
kwargs["driver"] = driver

kwargs.update(extra)
return ibis.singlestoredb.connect(**kwargs)


def _build_pyspark_connection(**config: t.Any) -> t.Any:
"""Build a PySpark ibis connection.

Expand Down Expand Up @@ -567,6 +607,12 @@ def _build_pyspark_connection(**config: t.Any) -> t.Any:
connection_string_scheme="",
connection_builder=_build_databricks_connection,
),
"singlestoredb": DialectSpec(
ibis_backend_name="singlestoredb",
connection_mode=_KWARGS,
connection_string_scheme="singlestoredb://",
connection_builder=_build_singlestoredb_connection,
),
"pyspark": DialectSpec(
ibis_backend_name="pyspark",
connection_mode=_CONNECTION_STRING,
Expand Down
3 changes: 3 additions & 0 deletions src/mountainash_data/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class CONST_DB_PROVIDER_TYPE(Enum):
ORACLE = auto()
CLICKHOUSE = auto()
DATABRICKS = auto()
SINGLESTOREDB = auto()
PYSPARK = auto()


Expand Down Expand Up @@ -112,6 +113,7 @@ class CONST_DB_BACKEND(StrEnum):
MYSQL = "MYSQL"
CLICKHOUSE = "CLICKHOUSE"
MOTHERDUCK = "MOTHERDUCK"
SINGLESTOREDB = "SINGLESTOREDB"
PYICEBERG = "PYICEBERG"
# POLARS = "POLARS"
# PANDAS = "PANDAS"
Expand Down Expand Up @@ -147,6 +149,7 @@ class CONST_DB_BACKEND_IBIS_PREFIX(StrEnum):
MSSQL = "mssql:"
CLICKHOUSE = "clickhouse:"
MYSQL = "mysql:"
SINGLESTOREDB = "singlestoredb:"

class CONST_DB_BACKEND_CAPABILITIES(Enum):
"""
Expand Down
4 changes: 3 additions & 1 deletion src/mountainash_data/core/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class body is a two-line shell (``__descriptor__`` + ``__adapter__``).
from .clickhouse import ClickHouseAuthSettings
from .databricks import DatabricksAuthSettings
from .mysql import MySQLAuthSettings
from .singlestoredb import SingleStoreDBAuthSettings
from .mssql import MSSQLAuthSettings
from .snowflake import SnowflakeAuthSettings
from .bigquery import BigQueryAuthSettings
Expand All @@ -61,7 +62,8 @@ class body is a two-line shell (``__descriptor__`` + ``__adapter__``).
# backends
"SQLiteAuthSettings", "DuckDBAuthSettings", "MotherDuckAuthSettings",
"PostgreSQLAuthSettings", "ClickHouseAuthSettings",
"DatabricksAuthSettings", "MySQLAuthSettings", "MSSQLAuthSettings",
"DatabricksAuthSettings", "MySQLAuthSettings", "SingleStoreDBAuthSettings",
"MSSQLAuthSettings",
"SnowflakeAuthSettings", "BigQueryAuthSettings", "RedshiftAuthSettings",
"PySparkAuthSettings", "TrinoAuthSettings", "PyIcebergRestAuthSettings",
]
51 changes: 51 additions & 0 deletions src/mountainash_data/core/settings/singlestoredb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""SingleStoreDB backend settings.

Driver: https://singlestoredb-python.labs.singlestore.com/
Ibis: ``ibis.singlestoredb.connect(host, user, password, port, database,
driver, autocommit, local_infile, **kwargs)``
"""

from __future__ import annotations

import typing as t
from enum import StrEnum

from ..constants import CONST_DB_PROVIDER_TYPE
from mountainash_settings.auth import NoAuth, PasswordAuth
from .descriptor import BackendDescriptor, ParameterSpec
from .profile import ConnectionProfile
from .registry import register


class SingleStoreDriver(StrEnum):
MYSQL = "mysql"
HTTP = "http"
HTTPS = "https"


SINGLESTOREDB_DESCRIPTOR = BackendDescriptor(
name="singlestoredb",
provider_type=CONST_DB_PROVIDER_TYPE.SINGLESTOREDB,
default_port=3306,
connection_string_scheme="singlestoredb://",
ibis_dialect="singlestoredb",
auth_modes=[PasswordAuth, NoAuth],
parameters=[
ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"),
ParameterSpec(name="PORT", type=int, tier="core", default=3306,
driver_key="port"),
ParameterSpec(name="DATABASE", type=t.Optional[str], tier="core",
default=None, driver_key="database"),
ParameterSpec(name="DRIVER", type=t.Optional[SingleStoreDriver],
tier="core", default=None, driver_key="driver"),
ParameterSpec(name="AUTOCOMMIT", type=bool, tier="core",
default=True, driver_key="autocommit"),
ParameterSpec(name="LOCAL_INFILE", type=bool, tier="advanced",
default=True, driver_key="local_infile"),
],
)


@register(SINGLESTOREDB_DESCRIPTOR)
class SingleStoreDBAuthSettings(ConnectionProfile):
__descriptor__ = SINGLESTOREDB_DESCRIPTOR
4 changes: 2 additions & 2 deletions tests/test_unit/backends/ibis/test_dialect_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ def fake_index_sql(table_name, index_name):
assert spec.get_index_exists_sql("users", "idx_users_id") == "SELECT 1 FROM users"


def test_registry_contains_all_14_backends():
def test_registry_contains_all_15_backends():
expected = {
"sqlite", "duckdb", "motherduck", "postgres", "mysql", "mssql",
"oracle", "snowflake", "bigquery", "redshift", "trino", "pyspark",
"clickhouse", "databricks",
"clickhouse", "databricks", "singlestoredb",
}
assert set(DIALECTS.keys()) == expected

Expand Down
74 changes: 74 additions & 0 deletions tests/test_unit/core/settings/backends/test_singlestoredb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# tests/test_unit/core/settings/backends/test_singlestoredb.py
from __future__ import annotations

import pytest
from pydantic import SecretStr, ValidationError

from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE
from mountainash_data.core.settings.auth import NoAuth, PasswordAuth
from mountainash_data.core.settings.singlestoredb import (
SingleStoreDBAuthSettings,
SingleStoreDriver,
)


@pytest.mark.unit
class TestSingleStoreDBAuthSettings:
def _minimal(self, **extra):
return SingleStoreDBAuthSettings(
HOST="svc-123.svc.singlestore.com",
auth=PasswordAuth(username="admin", password=SecretStr("s3cret")),
**extra,
)

def test_provider_type_is_singlestoredb(self):
s = self._minimal()
assert s.provider_type == CONST_DB_PROVIDER_TYPE.SINGLESTOREDB

def test_default_port(self):
s = self._minimal()
assert s.PORT == 3306

def test_custom_port(self):
s = self._minimal(PORT=3307)
assert s.PORT == 3307

def test_driver_enum_enforced(self):
with pytest.raises(ValidationError):
self._minimal(DRIVER="nonsense")

def test_driver_mysql(self):
s = self._minimal(DRIVER=SingleStoreDriver.MYSQL)
assert s.DRIVER == SingleStoreDriver.MYSQL

def test_driver_https(self):
s = self._minimal(DRIVER=SingleStoreDriver.HTTPS)
kwargs = s.to_driver_kwargs()
assert kwargs["driver"] == "https"

def test_autocommit_default_true(self):
s = self._minimal()
assert s.AUTOCOMMIT is True

def test_local_infile_default_true(self):
s = self._minimal()
assert s.LOCAL_INFILE is True

def test_no_auth(self):
s = SingleStoreDBAuthSettings(
HOST="svc-123.svc.singlestore.com", auth=NoAuth(),
)
assert s.HOST == "svc-123.svc.singlestore.com"

def test_to_driver_kwargs_plumbs_core_fields(self):
s = self._minimal(PORT=3307, DATABASE="mydb")
kwargs = s.to_driver_kwargs()
assert kwargs["host"] == "svc-123.svc.singlestore.com"
assert kwargs["port"] == 3307
assert kwargs["database"] == "mydb"
assert kwargs["user"] == "admin"
assert kwargs["password"] == "s3cret"

def test_ibis_dialect(self):
s = self._minimal()
assert s.backend == "singlestoredb"
Loading