diff --git a/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImpl.java b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImpl.java index e8100959a..9a32a451c 100644 --- a/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImpl.java +++ b/data-agent-management/src/main/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImpl.java @@ -115,9 +115,27 @@ public Datasource createDatasource(Datasource datasource) { @Override public Datasource updateDatasource(Integer id, Datasource datasource) { - // Regenerate connection URL + // 当连接参数发生变化时,需要重新生成连接 URL,避免继续保存客户端带回来的旧 URL。 + // 参数未变化时仍保留 resolveConnectionUrl 的行为,避免覆盖用户手动填写的自定义 URL。 DatasourceTypeHandler handler = datasourceTypeHandlerRegistry.getRequired(datasource.getType()); - String connectionUrl = handler.resolveConnectionUrl(datasource); + Datasource existing = datasourceMapper.selectById(id); + String connectionUrl; + if (existing != null && handler.hasRequiredConnectionFields(datasource) + && connectionParamsChanged(existing, datasource)) { + // 仅采用 handler 能完整拼出、且不会把复合 databaseName 泄漏进 URL 的重建结果。 + // 例如当前 Oracle 的 databaseName 可能是 "service|schema",#509 合并前朴素重建会把 + // "|schema" 拼进 URL;此时继续保留 resolveConnectionUrl 的结果。 + String rebuilt = handler.buildConnectionUrl(datasource); + if (StringUtils.isNotBlank(rebuilt) && !rebuilt.contains("|")) { + connectionUrl = rebuilt; + } + else { + connectionUrl = handler.resolveConnectionUrl(datasource); + } + } + else { + connectionUrl = handler.resolveConnectionUrl(datasource); + } if (StringUtils.isNotBlank(connectionUrl)) { datasource.setConnectionUrl(connectionUrl); } @@ -135,6 +153,16 @@ public Datasource updateDatasource(Integer id, Datasource datasource) { return datasource; } + /** + * 判断会影响自动生成连接 URL 的参数是否发生变化。 + */ + private boolean connectionParamsChanged(Datasource existing, Datasource incoming) { + return !Objects.equals(existing.getType(), incoming.getType()) + || !Objects.equals(existing.getHost(), incoming.getHost()) + || !Objects.equals(existing.getPort(), incoming.getPort()) + || !Objects.equals(existing.getDatabaseName(), incoming.getDatabaseName()); + } + @Override @Transactional public void deleteDatasource(Integer id) { diff --git a/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImplTest.java b/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImplTest.java index 9e46534ab..580b8c173 100644 --- a/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImplTest.java +++ b/data-agent-management/src/test/java/com/alibaba/cloud/ai/dataagent/service/datasource/impl/DatasourceServiceImplTest.java @@ -25,6 +25,9 @@ import com.alibaba.cloud.ai.dataagent.mapper.DatasourceMapper; import com.alibaba.cloud.ai.dataagent.mapper.LogicalRelationMapper; import com.alibaba.cloud.ai.dataagent.service.datasource.handler.DatasourceTypeHandler; +import com.alibaba.cloud.ai.dataagent.service.datasource.handler.impl.MysqlDatasourceTypeHandler; +import com.alibaba.cloud.ai.dataagent.service.datasource.handler.impl.OracleDatasourceTypeHandler; +import com.alibaba.cloud.ai.dataagent.service.datasource.handler.impl.PostgreSqlDatasourceTypeHandler; import com.alibaba.cloud.ai.dataagent.service.datasource.handler.registry.DatasourceTypeHandlerRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -423,6 +426,126 @@ void updateDatasource_nullUsernameAndPassword_setsEmpty() { assertEquals("", result.getPassword()); } + @Test + void updateDatasource_rebuildsConnectionUrlWhenDatabaseNameChanges() { + // #345:修改数据库名后,即使客户端带回旧 URL,也要按新的连接参数重新生成。 + Datasource existing = Datasource.builder() + .id(5) + .type("mysql") + .host("localhost") + .port(3306) + .databaseName("olddb") + .connectionUrl("jdbc:mysql://localhost:3306/olddb") + .build(); + when(datasourceMapper.selectById(5)).thenReturn(existing); + when(handlerRegistry.getRequired("mysql")).thenReturn(new MysqlDatasourceTypeHandler()); + + Datasource incoming = Datasource.builder() + .type("mysql") + .host("localhost") + .port(3306) + .databaseName("newdb") + .connectionUrl("jdbc:mysql://localhost:3306/olddb") + .build(); + + Datasource result = datasourceService.updateDatasource(5, incoming); + + assertTrue(result.getConnectionUrl().contains("/newdb"), + "URL should reflect the new database name but was: " + result.getConnectionUrl()); + assertFalse(result.getConnectionUrl().contains("/olddb"), "URL should not keep the old database name"); + verify(datasourceMapper).updateById(incoming); + } + + @Test + void updateDatasource_preservesCustomUrlWhenParamsUnchanged() { + // 连接参数未变化时,认为 connectionUrl 是用户手动填写的自定义 URL,不主动覆盖。 + Datasource existing = Datasource.builder() + .id(7) + .type("mysql") + .host("localhost") + .port(3306) + .databaseName("db") + .connectionUrl("jdbc:mysql://localhost:3306/db") + .build(); + when(datasourceMapper.selectById(7)).thenReturn(existing); + when(handlerRegistry.getRequired("mysql")).thenReturn(new MysqlDatasourceTypeHandler()); + + String customUrl = "jdbc:mysql://localhost:3306/db?useSSL=true&customParam=keepme"; + Datasource incoming = Datasource.builder() + .type("mysql") + .host("localhost") + .port(3306) + .databaseName("db") + .connectionUrl(customUrl) + .build(); + + Datasource result = datasourceService.updateDatasource(7, incoming); + + assertEquals(customUrl, result.getConnectionUrl()); + verify(datasourceMapper).updateById(incoming); + } + + @Test + void updateDatasource_preservesUrlForCompositeDatabaseName() { + // Oracle 把 schema 编码进 databaseName("service|schema"),朴素重建会把 + // "|schema" 残留进 URL,因此这类结果保留原 URL(完整重建见 #509)。 + Datasource existing = Datasource.builder() + .id(9) + .type("oracle") + .host("orahost") + .port(1521) + .databaseName("orcl|HR") + .connectionUrl("jdbc:oracle:thin:@orahost:1521/orcl") + .build(); + when(datasourceMapper.selectById(9)).thenReturn(existing); + when(handlerRegistry.getRequired("oracle")).thenReturn(new OracleDatasourceTypeHandler()); + + Datasource incoming = Datasource.builder() + .type("oracle") + .host("orahost") + .port(1521) + .databaseName("orcl|SCOTT") + .connectionUrl("jdbc:oracle:thin:@orahost:1521/orcl") + .build(); + + Datasource result = datasourceService.updateDatasource(9, incoming); + + assertEquals("jdbc:oracle:thin:@orahost:1521/orcl", result.getConnectionUrl()); + verify(datasourceMapper).updateById(incoming); + } + + @Test + void updateDatasource_rebuildsUrlForPostgresCompositeDatabaseName() { + // PostgreSQL 的 databaseName 同样是 "db|schema",但 buildConnectionUrl 会切出 db。 + // 重建结果不含 "|" 时应正常采用,避免误伤 PostgreSQL 的 #345 场景。 + Datasource existing = Datasource.builder() + .id(11) + .type("postgresql") + .host("pghost") + .port(5432) + .databaseName("olddb|public") + .connectionUrl("jdbc:postgresql://pghost:5432/olddb") + .build(); + when(datasourceMapper.selectById(11)).thenReturn(existing); + when(handlerRegistry.getRequired("postgresql")).thenReturn(new PostgreSqlDatasourceTypeHandler()); + + Datasource incoming = Datasource.builder() + .type("postgresql") + .host("pghost") + .port(5432) + .databaseName("newdb|public") + .connectionUrl("jdbc:postgresql://pghost:5432/olddb") + .build(); + + Datasource result = datasourceService.updateDatasource(11, incoming); + + assertTrue(result.getConnectionUrl().contains("/newdb"), + "URL should reflect the new database but was: " + result.getConnectionUrl()); + assertFalse(result.getConnectionUrl().contains("olddb"), "URL should not keep the old database"); + assertFalse(result.getConnectionUrl().contains("|"), "schema separator must not leak into the URL"); + verify(datasourceMapper).updateById(incoming); + } + @Test void testConnection_pingFails_returnsFalse() { Datasource ds = Datasource.builder().id(1).name("test").type("mysql").username("user").password("pass").build();