This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 838376dbc28 fix(mongodb): treat the Mongo database as the SQL Lab 
schema so Data Preview returns rows (#44141)
838376dbc28 is described below

commit 838376dbc281f518ca1108a8a224c0793bd62240
Author: Mehmet Salih Yavuz <[email protected]>
AuthorDate: Wed Sep 23 16:23:21 2026 +0300

    fix(mongodb): treat the Mongo database as the SQL Lab schema so Data 
Preview returns rows (#44141)
---
 superset/db_engine_specs/mongodb.py              |  63 ++++++-
 tests/unit_tests/db_engine_specs/test_mongodb.py | 205 ++++++++++++++++++++++-
 2 files changed, 266 insertions(+), 2 deletions(-)

diff --git a/superset/db_engine_specs/mongodb.py 
b/superset/db_engine_specs/mongodb.py
index 03ea7dc4b92..28c41759525 100644
--- a/superset/db_engine_specs/mongodb.py
+++ b/superset/db_engine_specs/mongodb.py
@@ -23,12 +23,19 @@ to enable SQL queries on MongoDB collections.
 from __future__ import annotations
 
 from datetime import datetime
-from typing import Any, Optional
+from typing import Any, Optional, TYPE_CHECKING
 
 from sqlalchemy import types
+from sqlalchemy.engine.interfaces import Dialect
+from sqlalchemy.engine.url import URL
 
 from superset.constants import TimeGrain
+from superset.databases.utils import make_url_safe
 from superset.db_engine_specs.base import BaseEngineSpec, DatabaseCategory
+from superset.sql.parse import Table
+
+if TYPE_CHECKING:
+    from superset.models.core import Database
 
 
 class MongoDBEngineSpec(BaseEngineSpec):
@@ -38,6 +45,13 @@ class MongoDBEngineSpec(BaseEngineSpec):
     engine_name = "MongoDB"
     force_column_alias_quotes = False
 
+    # A MongoDB database is exposed as a SQLAlchemy schema. PyMongoSQL treats 
the
+    # whole FROM reference as the collection name, so the schema is selected 
through
+    # the driver's ``database`` connect argument rather than by qualifying the
+    # collection. The URI database is left untouched because MongoDB also uses 
it as
+    # the default ``authSource``.
+    supports_dynamic_schema = True
+
     metadata = {
         "description": ("MongoDB is a document-oriented, operational NoSQL 
database."),
         "logo": "mongodb.png",
@@ -102,6 +116,53 @@ class MongoDBEngineSpec(BaseEngineSpec):
         ),
     }
 
+    @classmethod
+    def adjust_engine_params(
+        cls,
+        uri: URL,
+        connect_args: dict[str, Any],
+        catalog: Optional[str] = None,
+        schema: Optional[str] = None,
+    ) -> tuple[URL, dict[str, Any]]:
+        uri, new_connect_args = super().adjust_engine_params(
+            uri,
+            connect_args,
+            catalog,
+            schema,
+        )
+
+        if schema:
+            new_connect_args = {**new_connect_args, "database": schema}
+
+        return uri, new_connect_args
+
+    @classmethod
+    def get_schema_from_engine_params(
+        cls,
+        sqlalchemy_uri: URL,
+        connect_args: dict[str, Any],
+    ) -> Optional[str]:
+        # PyMongoSQL gives the ``database`` connect argument precedence over 
the URI.
+        return connect_args.get("database") or sqlalchemy_uri.database or None
+
+    @classmethod
+    def get_default_schema(
+        cls,
+        database: Database,
+        catalog: Optional[str],
+    ) -> Optional[str]:
+        return cls.get_schema_from_engine_params(
+            make_url_safe(database.sqlalchemy_uri), database.connect_args
+        )
+
+    @classmethod
+    def quote_table(cls, table: Table, dialect: Dialect) -> str:
+        """
+        Quote only the collection name. PyMongoSQL treats the whole FROM 
reference
+        as the collection, so the schema is applied via the ``database`` 
connect arg.
+        """
+        return dialect.identifier_preparer.quote(table.table)
+
     @classmethod
     def epoch_to_dttm(cls) -> str:
         return "datetime({col}, 'unixepoch')"
diff --git a/tests/unit_tests/db_engine_specs/test_mongodb.py 
b/tests/unit_tests/db_engine_specs/test_mongodb.py
index 2272fd51610..8a481daa082 100644
--- a/tests/unit_tests/db_engine_specs/test_mongodb.py
+++ b/tests/unit_tests/db_engine_specs/test_mongodb.py
@@ -15,11 +15,13 @@
 # specific language governing permissions and limitations
 # under the License.
 from datetime import datetime
-from typing import Optional
+from typing import Any, Optional
 
 import pytest
+from pytest_mock import MockerFixture
 
 from superset.constants import TimeGrain
+from superset.utils import json
 from tests.unit_tests.db_engine_specs.utils import assert_convert_dttm
 from tests.unit_tests.fixtures.common import dttm  # noqa: F401
 
@@ -123,3 +125,204 @@ def test_engine_metadata() -> None:
     assert spec.engine == "mongodb"
     assert spec.engine_name == "MongoDB"
     assert spec.force_column_alias_quotes is False
+    assert spec.supports_dynamic_schema is True
+
+
[email protected](
+    "connect_args,schema,expected",
+    [
+        ({"foo": "bar"}, "dbtwo", {"foo": "bar", "database": "dbtwo"}),
+        ({"foo": "bar"}, None, {"foo": "bar"}),
+        (
+            {"database": "dbone", "authSource": "dbone"},
+            "dbtwo",
+            {"database": "dbtwo", "authSource": "dbone"},
+        ),
+    ],
+)
+def test_adjust_engine_params(
+    connect_args: dict[str, Any],
+    schema: Optional[str],
+    expected: dict[str, Any],
+) -> None:
+    """
+    The selected schema is applied through the ``database`` connect argument 
and
+    the URI is left untouched, since its database is the default 
``authSource``.
+    """
+    from sqlalchemy.engine.url import make_url
+
+    from superset.db_engine_specs.mongodb import MongoDBEngineSpec
+
+    uri = "mongodb://user:pass@host:27017/dbone?mode=superset"
+    original = dict(connect_args)
+
+    adjusted, new_connect_args = MongoDBEngineSpec.adjust_engine_params(
+        make_url(uri), connect_args, schema=schema
+    )
+
+    assert adjusted.render_as_string(hide_password=False) == uri
+    assert new_connect_args == expected
+    assert connect_args == original
+
+
[email protected](
+    "uri,connect_args,expected_auth_source",
+    [
+        ("mongodb://user:pass@host:27017/dbone?mode=superset", {}, "dbone"),
+        (
+            
"mongodb://user:pass@host:27017/dbone?mode=superset&authSource=admin",
+            {},
+            "admin",
+        ),
+        (
+            "mongodb://user:pass@host:27017/dbone?mode=superset",
+            {"database": "dbone", "authSource": "dbone"},
+            "dbone",
+        ),
+    ],
+)
+def test_adjust_engine_params_driver(
+    uri: str,
+    connect_args: dict[str, Any],
+    expected_auth_source: str,
+) -> None:
+    """
+    With the real driver, selecting ``dbtwo`` queries ``dbtwo`` while keeping 
the
+    original authentication database.
+    """
+    pytest.importorskip("pymongosql")
+
+    from sqlalchemy import create_engine
+    from sqlalchemy.engine.url import make_url
+
+    from superset.db_engine_specs.mongodb import MongoDBEngineSpec
+
+    adjusted, new_connect_args = MongoDBEngineSpec.adjust_engine_params(
+        make_url(uri), connect_args, schema="dbtwo"
+    )
+    engine = create_engine(
+        adjusted, connect_args={**new_connect_args, "connect": False}
+    )
+    connection = engine.raw_connection().driver_connection
+    try:
+        credentials = connection.client.options.pool_options._credentials
+        assert connection.database_name == "dbtwo"
+        assert credentials.source == expected_auth_source
+    finally:
+        connection.close()
+
+
+def test_get_schema_from_engine_params() -> None:
+    from sqlalchemy.engine.url import make_url
+
+    from superset.db_engine_specs.mongodb import MongoDBEngineSpec
+
+    assert (
+        MongoDBEngineSpec.get_schema_from_engine_params(
+            make_url("mongodb://user:pass@host:27017/dbone?mode=superset"), {}
+        )
+        == "dbone"
+    )
+    assert (
+        MongoDBEngineSpec.get_schema_from_engine_params(
+            make_url("mongodb://user:pass@host:27017"), {}
+        )
+        is None
+    )
+    assert (
+        MongoDBEngineSpec.get_schema_from_engine_params(
+            make_url("mongodb://user:pass@host:27017/dbone?mode=superset"),
+            {"database": "dbtwo"},
+        )
+        == "dbtwo"
+    )
+
+
+def test_get_default_schema() -> None:
+    from superset.db_engine_specs.mongodb import MongoDBEngineSpec
+    from superset.models.core import Database
+
+    database = Database(
+        database_name="mongo",
+        sqlalchemy_uri="mongodb://user:pass@host:27017/dbone?mode=superset",
+    )
+
+    assert MongoDBEngineSpec.get_default_schema(database, None) == "dbone"
+
+    database.extra = json.dumps(
+        {"engine_params": {"connect_args": {"database": "dbtwo"}}}
+    )
+    assert MongoDBEngineSpec.get_default_schema(database, None) == "dbtwo"
+
+
+def test_get_default_schema_for_query(mocker: MockerFixture) -> None:
+    """
+    Access checks must resolve unqualified collections against the query 
schema,
+    which is the database the connection is bound to.
+    """
+    from superset.db_engine_specs.mongodb import MongoDBEngineSpec
+    from superset.models.core import Database
+
+    database = Database(
+        database_name="mongo",
+        sqlalchemy_uri="mongodb://user:pass@host:27017/dbone?mode=superset",
+    )
+    query = mocker.MagicMock(schema="dbtwo", catalog=None)
+
+    assert MongoDBEngineSpec.get_default_schema_for_query(database, query) == 
"dbtwo"
+
+
+def test_select_star_does_not_qualify_collection(mocker: MockerFixture) -> 
None:
+    """
+    PyMongoSQL treats ``schema.collection`` as a literal collection name, so 
the
+    preview query must reference the bare collection and rely on the schema
+    being applied to the connection instead.
+    """
+    from sqlalchemy.engine.default import DefaultDialect
+
+    from superset.db_engine_specs.mongodb import MongoDBEngineSpec
+    from superset.sql.parse import Table
+
+    database = mocker.MagicMock()
+    database.compile_sqla_query.side_effect = lambda qry, catalog, schema: str(
+        qry.compile(dialect=DefaultDialect(), compile_kwargs={"literal_binds": 
True})
+    )
+
+    sql = MongoDBEngineSpec.select_star(
+        database,
+        Table("orders", "testdb"),
+        DefaultDialect(),
+        limit=10,
+        show_cols=False,
+        latest_partition=False,
+    )
+
+    assert sql == "SELECT\n  *\nFROM orders\nLIMIT 10"
+    database.compile_sqla_query.assert_called_once()
+    assert database.compile_sqla_query.call_args.args[1:] == (None, "testdb")
+
+
+def test_get_sqla_engine_applies_selected_schema() -> None:
+    """
+    The ``database`` connect argument returned by ``adjust_engine_params`` must
+    reach the driver when the engine is built through ``Database``.
+    """
+    pytest.importorskip("pymongosql")
+
+    from superset.models.core import Database
+
+    database = Database(
+        database_name="mongo",
+        sqlalchemy_uri="mongodb://user:pass@host:27017/dbone?mode=superset",
+        extra=json.dumps({"engine_params": {"connect_args": {"connect": 
False}}}),
+    )
+
+    with database.get_sqla_engine(schema="dbtwo") as engine:
+        raw_connection = engine.raw_connection()
+        try:
+            connection = raw_connection.driver_connection
+            credentials = connection.client.options.pool_options._credentials
+            assert connection.database_name == "dbtwo"
+            assert credentials.source == "dbone"
+        finally:
+            raw_connection.close()

Reply via email to