Skip to content

Commit d88cba9

Browse files
feat: optimize catalog permission sync (apache#33000)
1 parent 5304bed commit d88cba9

File tree

9 files changed

+58
-30
lines changed

9 files changed

+58
-30
lines changed

superset/commands/database/sync_permissions.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,7 @@ def sync_database_permissions(self) -> None:
140140
"""
141141
Syncs the permissions for a DB connection.
142142
"""
143-
catalogs = (
144-
self._get_catalog_names()
145-
if self.db_connection.db_engine_spec.supports_catalog
146-
else [None]
147-
)
148-
149-
for catalog in catalogs:
143+
for catalog in self._get_catalog_names():
150144
try:
151145
schemas = self._get_schema_names(catalog)
152146

@@ -192,15 +186,29 @@ def sync_database_permissions(self) -> None:
192186
if self.old_db_connection_name != self.db_connection.database_name:
193187
self._rename_database_in_permissions(catalog, schemas)
194188

195-
def _get_catalog_names(self) -> set[str]:
189+
def _get_catalog_names(self) -> set[str | None]:
196190
"""
197191
Helper method to load catalogs.
198192
"""
193+
if not self.db_connection.db_engine_spec.supports_catalog:
194+
return {None}
195+
199196
try:
200-
return self.db_connection.get_all_catalog_names(
201-
force=True,
202-
ssh_tunnel=self.db_connection_ssh_tunnel,
203-
)
197+
# Adding permissions to all catalogs (and all their schemas) can take a long
198+
# time (minutes, while importing a chart, eg). If the database does not
199+
# support cross-catalog queries (like Postgres), and the multi-catalog
200+
# feature is not enabled, then we only need to add permissions to the
201+
# default catalog.
202+
if (
203+
self.db_connection.db_engine_spec.supports_cross_catalog_queries
204+
or self.db_connection.allow_multi_catalog
205+
):
206+
return self.db_connection.get_all_catalog_names(
207+
force=True,
208+
ssh_tunnel=self.db_connection_ssh_tunnel,
209+
)
210+
else:
211+
return {self.db_connection.get_default_catalog()}
204212
except OAuth2RedirectError:
205213
# raise OAuth2 exceptions as-is
206214
raise

superset/db_engine_specs/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
428428
# Can the catalog be changed on a per-query basis?
429429
supports_dynamic_catalog = False
430430

431+
# Does the DB engine spec support cross-catalog queries?
432+
supports_cross_catalog_queries = False
433+
431434
# Does the engine supports OAuth 2.0? This requires logic to be added to one of the
432435
# the user impersonation methods to handle personal tokens.
433436
supports_oauth2 = False

superset/db_engine_specs/bigquery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class BigQueryEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-met
136136

137137
allows_hidden_cc_in_orderby = True
138138

139-
supports_catalog = supports_dynamic_catalog = True
139+
supports_catalog = supports_dynamic_catalog = supports_cross_catalog_queries = True
140140

141141
# when editing the database, mask this field in `encrypted_extra`
142142
# pylint: disable=invalid-name
@@ -539,7 +539,7 @@ def estimate_query_cost( # pylint: disable=too-many-arguments
539539
]
540540

541541
@classmethod
542-
def get_default_catalog(cls, database: Database) -> str | None:
542+
def get_default_catalog(cls, database: Database) -> str:
543543
"""
544544
Get the default catalog.
545545
"""

superset/db_engine_specs/databricks.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,10 @@ class DatabricksNativeEngineSpec(DatabricksDynamicBaseEngineSpec):
373373
"extra",
374374
}
375375

376-
supports_dynamic_schema = supports_catalog = supports_dynamic_catalog = True
376+
supports_dynamic_schema = True
377+
supports_catalog = True
378+
supports_dynamic_catalog = True
379+
supports_cross_catalog_queries = True
377380

378381
@classmethod
379382
def build_sqlalchemy_uri( # type: ignore
@@ -433,10 +436,7 @@ def parameters_json_schema(cls) -> Any:
433436
return spec.to_dict()["components"]["schemas"][cls.__name__]
434437

435438
@classmethod
436-
def get_default_catalog(
437-
cls,
438-
database: Database,
439-
) -> str | None:
439+
def get_default_catalog(cls, database: Database) -> str:
440440
"""
441441
Return the default catalog.
442442

superset/db_engine_specs/doris.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class DorisEngineSpec(MySQLEngineSpec):
113113
)
114114
encryption_parameters = {"ssl": "0"}
115115
supports_dynamic_schema = True
116-
supports_catalog = supports_dynamic_catalog = True
116+
supports_catalog = supports_dynamic_catalog = supports_cross_catalog_queries = True
117117

118118
column_type_mappings = ( # type: ignore
119119
(

superset/db_engine_specs/postgres.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def adjust_engine_params(
319319
return uri, connect_args
320320

321321
@classmethod
322-
def get_default_catalog(cls, database: Database) -> str | None:
322+
def get_default_catalog(cls, database: Database) -> str:
323323
"""
324324
Return the default catalog for a given database.
325325
"""

superset/db_engine_specs/presto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class PrestoBaseEngineSpec(BaseEngineSpec, metaclass=ABCMeta):
162162
"""
163163

164164
supports_dynamic_schema = True
165-
supports_catalog = supports_dynamic_catalog = True
165+
supports_catalog = supports_dynamic_catalog = supports_cross_catalog_queries = True
166166

167167
column_type_mappings = (
168168
(

superset/db_engine_specs/snowflake.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class SnowflakeEngineSpec(PostgresBaseEngineSpec):
8888
sqlalchemy_uri_placeholder = "snowflake://"
8989

9090
supports_dynamic_schema = True
91-
supports_catalog = supports_dynamic_catalog = True
91+
supports_catalog = supports_dynamic_catalog = supports_cross_catalog_queries = True
9292

9393
# pylint: disable=invalid-name
9494
encrypted_extra_sensitive_fields = {
@@ -189,7 +189,7 @@ def get_schema_from_engine_params(
189189
return parse.unquote(database.split("/")[1])
190190

191191
@classmethod
192-
def get_default_catalog(cls, database: "Database") -> Optional[str]:
192+
def get_default_catalog(cls, database: "Database") -> str:
193193
"""
194194
Return the default catalog.
195195
"""

tests/unit_tests/commands/databases/sync_permissions_test.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,31 @@ def test_sync_permissions_command_async_mode_new_db_name(
231231
async_task_mock.delay.assert_called_once_with(1, "admin", "Old Name")
232232

233233

234-
def test_resync_permissions_command_get_catalogs(database_with_catalog: MagicMock):
234+
def test_sync_permissions_command_get_catalogs(database_with_catalog: MagicMock):
235235
"""
236236
Test the ``_get_catalog_names`` method.
237237
"""
238238
cmmd = SyncPermissionsCommand(1, None, db_connection=database_with_catalog)
239239
assert cmmd._get_catalog_names() == ["catalog1", "catalog2"]
240240

241241

242+
def test_sync_permissions_command_get_default_catalog(database_with_catalog: MagicMock):
243+
"""
244+
Test ``_get_catalog_names`` when only the default one should be returned.
245+
246+
When the database doesn't not support cross-catalog queries (like Postgres), we
247+
should only return all catalogs if multi-catalog is enabled.
248+
"""
249+
database_with_catalog.db_engine_spec.supports_cross_catalog_queries = False
250+
database_with_catalog.allow_multi_catalog = False
251+
cmmd = SyncPermissionsCommand(1, None, db_connection=database_with_catalog)
252+
assert cmmd._get_catalog_names() == {"catalog2"}
253+
254+
database_with_catalog.allow_multi_catalog = True
255+
cmmd = SyncPermissionsCommand(1, None, db_connection=database_with_catalog)
256+
assert cmmd._get_catalog_names() == ["catalog1", "catalog2"]
257+
258+
242259
@pytest.mark.parametrize(
243260
("inner_exception, outer_exception"),
244261
[
@@ -249,7 +266,7 @@ def test_resync_permissions_command_get_catalogs(database_with_catalog: MagicMoc
249266
(GenericDBException, DatabaseConnectionFailedError),
250267
],
251268
)
252-
def test_resync_permissions_command_raise_on_getting_catalogs(
269+
def test_sync_permissions_command_raise_on_getting_catalogs(
253270
inner_exception: Exception,
254271
outer_exception: Exception,
255272
database_with_catalog: MagicMock,
@@ -263,7 +280,7 @@ def test_resync_permissions_command_raise_on_getting_catalogs(
263280
cmmd._get_catalog_names()
264281

265282

266-
def test_resync_permissions_command_get_schemas(database_with_catalog: MagicMock):
283+
def test_sync_permissions_command_get_schemas(database_with_catalog: MagicMock):
267284
"""
268285
Test the ``_get_schema_names`` method.
269286
"""
@@ -282,7 +299,7 @@ def test_resync_permissions_command_get_schemas(database_with_catalog: MagicMock
282299
(GenericDBException, DatabaseConnectionFailedError),
283300
],
284301
)
285-
def test_resync_permissions_command_raise_on_getting_schemas(
302+
def test_sync_permissions_command_raise_on_getting_schemas(
286303
inner_exception: Exception,
287304
outer_exception: Exception,
288305
database_with_catalog: MagicMock,
@@ -296,7 +313,7 @@ def test_resync_permissions_command_raise_on_getting_schemas(
296313
cmmd._get_schema_names("blah")
297314

298315

299-
def test_resync_permissions_command_refresh_schemas(
316+
def test_sync_permissions_command_refresh_schemas(
300317
mocker: MockerFixture, database_with_catalog: MagicMock
301318
):
302319
"""
@@ -319,7 +336,7 @@ def test_resync_permissions_command_refresh_schemas(
319336
)
320337

321338

322-
def test_resync_permissions_command_rename_db_in_perms(
339+
def test_sync_permissions_command_rename_db_in_perms(
323340
mocker: MockerFixture, database_with_catalog: MagicMock
324341
):
325342
"""

0 commit comments

Comments
 (0)