diff --git a/libs/db-browser/pom.xml b/libs/db-browser/pom.xml index da75aab2fc..b4bc9adb6d 100644 --- a/libs/db-browser/pom.xml +++ b/libs/db-browser/pom.xml @@ -107,6 +107,7 @@ 21.1.0.0 2.8.0 42.7.3 + 12.8.2.jre8 @@ -226,6 +227,11 @@ postgresql ${postgres.jdbc.version} + + com.microsoft.sqlserver + mssql-jdbc + ${mssql.jdbc.version} + diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/AbstractDBBrowserFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/AbstractDBBrowserFactory.java index 104019e283..60b2c62053 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/AbstractDBBrowserFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/AbstractDBBrowserFactory.java @@ -44,6 +44,8 @@ public T create() { return buildForOdpSharding(); case POSTGRESQL: return buildForPostgres(); + case SQL_SERVER: + return buildForSqlServer(); default: throw new IllegalStateException("Not supported for the type, " + type); } @@ -63,4 +65,6 @@ public T create() { public abstract T buildForPostgres(); + public abstract T buildForSqlServer(); + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/DBBrowserFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/DBBrowserFactory.java index eaddb91c90..f4e8539985 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/DBBrowserFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/DBBrowserFactory.java @@ -24,6 +24,7 @@ public interface DBBrowserFactory { String DORIS = "DORIS"; String ODP_SHARDING_OB_MYSQL = "ODP_SHARDING_OB_MYSQL"; String POSTGRESQL = "POSTGRESQL"; + String SQL_SERVER = "SQL_SERVER"; T create(); diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewEditorFactory.java index b68e9f4cbc..954ebff27c 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewEditorFactory.java @@ -70,6 +70,11 @@ public DBMViewEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBMViewEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + private DBTableIndexEditor getMViewIndexEditor() { DBMViewIndexEditorFactory indexFactory = new DBMViewIndexEditorFactory(); indexFactory.setType(this.type); diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewIndexEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewIndexEditorFactory.java index 49a4b47ad7..20a1133513 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewIndexEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBMViewIndexEditorFactory.java @@ -60,4 +60,9 @@ public DBTableIndexEditor buildForOdpSharding() { public DBTableIndexEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + + @Override + public DBTableIndexEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java index 4a027b9b40..5bf6d726c4 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBObjectOperatorFactory.java @@ -69,6 +69,11 @@ public DBObjectOperator buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectOperator buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + private JdbcOperations getJdbcOperations() { if (this.jdbcOperations != null) { return this.jdbcOperations; diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSequenceEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSequenceEditorFactory.java index bb561fa582..cb2e69bb45 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSequenceEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSequenceEditorFactory.java @@ -56,4 +56,9 @@ public DBObjectEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSynonymEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSynonymEditorFactory.java index c44b9ee533..337f5ee9a6 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSynonymEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBSynonymEditorFactory.java @@ -56,4 +56,9 @@ public DBObjectEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java index e4cd7329ad..fe29e3061b 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableColumnEditorFactory.java @@ -56,4 +56,9 @@ public DBTableColumnEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBTableColumnEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java index 18dd00ae0e..2e01b46101 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableConstraintEditorFactory.java @@ -76,4 +76,9 @@ public DBTableConstraintEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBTableConstraintEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java index 19103a4926..52761cd88e 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableEditorFactory.java @@ -83,6 +83,11 @@ public DBTableEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBTableEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + private DBTableIndexEditor getTableIndexEditor() { DBTableIndexEditorFactory indexFactory = new DBTableIndexEditorFactory(); indexFactory.setType(this.type); diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java index 85451e17fc..3a9c38c095 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTableIndexEditorFactory.java @@ -20,6 +20,7 @@ import com.oceanbase.tools.dbbrowser.editor.mysql.OBMySQLIndexEditor; import com.oceanbase.tools.dbbrowser.editor.oracle.OBOracleIndexEditor; import com.oceanbase.tools.dbbrowser.editor.oracle.OracleIndexEditor; +import com.oceanbase.tools.dbbrowser.editor.sqlserver.SqlServerIndexEditor; import lombok.Setter; import lombok.experimental.Accessors; @@ -63,4 +64,9 @@ public DBTableIndexEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBTableIndexEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java index 276a4161f4..0633bea916 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/editor/DBTablePartitionEditorFactory.java @@ -81,4 +81,9 @@ public DBTablePartitionEditor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBTablePartitionEditor buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBConstraintType.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBConstraintType.java index f97b0fb458..af005ce11a 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBConstraintType.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBConstraintType.java @@ -25,6 +25,7 @@ public enum DBConstraintType { INDEX("INDEX"), CHECK("CHECK", "C"), NOT_NULL("NOT NULL", "NOT_NULL"), + UNIQUE("UNIQUE"), UNKNOWN("UNKNOWN"), ; diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBIndexType.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBIndexType.java index 3e14a33a7f..439f960870 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBIndexType.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/model/DBIndexType.java @@ -28,6 +28,7 @@ public enum DBIndexType { FUNCTION_BASED_BITMAP("FUNCTION-BASED BITMAP"), DOMAIN("DOMAIN"), SPATIAL("SPATIAL"), + CLUSTERED("CLUSTERED"), UNKNOWN("UNKNOWN"); private String value; diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/DBSchemaAccessorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/DBSchemaAccessorFactory.java index bef1ff9411..15d13807b6 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/DBSchemaAccessorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/DBSchemaAccessorFactory.java @@ -43,6 +43,7 @@ import com.oceanbase.tools.dbbrowser.schema.oracle.OBOracleSchemaAccessor; import com.oceanbase.tools.dbbrowser.schema.oracle.OracleSchemaAccessor; import com.oceanbase.tools.dbbrowser.schema.postgre.PostgresSchemaAccessor; +import com.oceanbase.tools.dbbrowser.schema.sqlserver.SqlServerSchemaAccessor; import com.oceanbase.tools.dbbrowser.util.ALLDataDictTableNames; import com.oceanbase.tools.dbbrowser.util.VersionUtils; @@ -161,6 +162,11 @@ public DBSchemaAccessor buildForPostgres() { return new PostgresSchemaAccessor(getJdbcOperations()); } + @Override + public DBSchemaAccessor buildForSqlServer() { + return new SqlServerSchemaAccessor(getJdbcOperations()); + } + private JdbcOperations getJdbcOperations() { if (this.jdbcOperations != null) { return this.jdbcOperations; diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/sqlserver/SqlServerSchemaAccessor.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/sqlserver/SqlServerSchemaAccessor.java new file mode 100644 index 0000000000..19874bd45d --- /dev/null +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/schema/sqlserver/SqlServerSchemaAccessor.java @@ -0,0 +1,1782 @@ +/* + * Copyright (c) 2025 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.tools.dbbrowser.schema.sqlserver; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcOperations; + +import com.oceanbase.tools.dbbrowser.model.DBColumnGroupElement; +import com.oceanbase.tools.dbbrowser.model.DBConstraintType; +import com.oceanbase.tools.dbbrowser.model.DBDatabase; +import com.oceanbase.tools.dbbrowser.model.DBFunction; +import com.oceanbase.tools.dbbrowser.model.DBIndexType; +import com.oceanbase.tools.dbbrowser.model.DBMViewRefreshParameter; +import com.oceanbase.tools.dbbrowser.model.DBMViewRefreshRecord; +import com.oceanbase.tools.dbbrowser.model.DBMViewRefreshRecordParam; +import com.oceanbase.tools.dbbrowser.model.DBMaterializedView; +import com.oceanbase.tools.dbbrowser.model.DBObjectIdentity; +import com.oceanbase.tools.dbbrowser.model.DBObjectType; +import com.oceanbase.tools.dbbrowser.model.DBPLObjectIdentity; +import com.oceanbase.tools.dbbrowser.model.DBPLParam; +import com.oceanbase.tools.dbbrowser.model.DBPLParamMode; +import com.oceanbase.tools.dbbrowser.model.DBPackage; +import com.oceanbase.tools.dbbrowser.model.DBProcedure; +import com.oceanbase.tools.dbbrowser.model.DBSequence; +import com.oceanbase.tools.dbbrowser.model.DBSynonym; +import com.oceanbase.tools.dbbrowser.model.DBSynonymType; +import com.oceanbase.tools.dbbrowser.model.DBTable; +import com.oceanbase.tools.dbbrowser.model.DBTable.DBTableOptions; +import com.oceanbase.tools.dbbrowser.model.DBTableColumn; +import com.oceanbase.tools.dbbrowser.model.DBTableConstraint; +import com.oceanbase.tools.dbbrowser.model.DBTableIndex; +import com.oceanbase.tools.dbbrowser.model.DBTablePartition; +import com.oceanbase.tools.dbbrowser.model.DBTablePartitionDefinition; +import com.oceanbase.tools.dbbrowser.model.DBTablePartitionOption; +import com.oceanbase.tools.dbbrowser.model.DBTableSubpartitionDefinition; +import com.oceanbase.tools.dbbrowser.model.DBTrigger; +import com.oceanbase.tools.dbbrowser.model.DBType; +import com.oceanbase.tools.dbbrowser.model.DBVariable; +import com.oceanbase.tools.dbbrowser.model.DBView; +import com.oceanbase.tools.dbbrowser.model.DBViewCheckOption; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; +import com.oceanbase.tools.dbbrowser.util.StringUtils; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SqlServerSchemaAccessor implements DBSchemaAccessor { + + protected JdbcOperations jdbcOperations; + + public SqlServerSchemaAccessor(@NonNull JdbcOperations jdbcOperations) { + this.jdbcOperations = jdbcOperations; + } + + @Override + public List showDatabases() { + String sql = "SELECT name FROM sys.databases;"; + try { + return jdbcOperations.queryForList(sql, String.class); + } catch (BadSqlGrammarException e) { + log.warn("Failed to query databases", e); + return Collections.emptyList(); + } + } + + @Override + public DBDatabase getDatabase(String schemaName) { + // 注意:在 SQL Server 中,schemaName 参数实际表示数据库名(database name) + // SQL Server 的层次结构:databases -> schemas -> tables + DBDatabase database = new DBDatabase(); + database.setId(schemaName); + database.setName(schemaName); + // SQL Server 中获取指定数据库的字符集和排序规则 + String sql = "SELECT " + + " collation_name AS collation " + + "FROM sys.databases " + + "WHERE name = ?"; + AtomicReference collation = new AtomicReference<>(); + try { + jdbcOperations.query(sql, new Object[] {schemaName}, rs -> { + if (rs.next()) { + collation.set(rs.getString(1)); + } + }); + database.setCollation(collation.get()); + } catch (Exception e) { + log.warn("Failed to get database collation for database: " + schemaName, e); + } + return database; + } + + @Override + public List listDatabases() { + // SQL Server 中返回所有数据库及其信息 + String sql = "SELECT " + + " name, " + + " collation_name AS collation " + + "FROM sys.databases " + + "WHERE state_desc = 'ONLINE' " + + "ORDER BY name"; + try { + return jdbcOperations.query(sql, (rs, rowNum) -> { + DBDatabase database = new DBDatabase(); + database.setId(rs.getString("name")); + database.setName(rs.getString("name")); + database.setCollation(rs.getString("collation")); + return database; + }); + } catch (Exception e) { + log.warn("Failed to list databases", e); + return Collections.emptyList(); + } + } + + @Override + public void switchDatabase(String schemaName) { + // SQL Server 使用 USE 语句切换数据库 + // 注意:参数名是 schemaName,但在 SQL Server 中实际是数据库名 + String sql = "USE [" + schemaName.replace("]", "]]") + "]"; + try { + jdbcOperations.execute(sql); + } catch (Exception e) { + log.error("Failed to switch database: " + schemaName, e); + throw new RuntimeException("Failed to switch database: " + schemaName, e); + } + } + + @Override + public List listUsers() { + return Collections.emptyList(); + } + + /** + * 解析 schemaName 参数,支持两种格式: 1. "database" - 只有数据库名,默认使用 dbo schema 2. "database.schema" - 数据库名和 + * schema 名 + * + * @param schemaName 可能是数据库名或 database.schema 格式 + * @return [databaseName, schemaName] + */ + private String[] parseDatabaseAndSchema(String schemaName) { + if (schemaName == null) { + return new String[] {"", "dbo"}; + } + + if (schemaName.contains(".")) { + String[] parts = schemaName.split("\\.", 2); + return new String[] {parts[0], parts[1]}; + } else { + // 兼容旧代码:只有数据库名,默认使用 dbo + return new String[] {schemaName, "dbo"}; + } + } + + @Override + public List showTables(String schemaName) { + return showTablesLike(schemaName, null); + } + + @Override + public List showTablesLike(String schemaName, String tableNameLike) { + // SQL Server 的层次结构:databases -> schemas -> tables + // schemaName 参数支持两种格式: + // 1. "database" - 只有数据库名,默认使用 dbo schema + // 2. "database.schema" - 数据库名和 schema 名 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + StringBuilder sb = new StringBuilder(); + sb.append("SELECT table_name FROM information_schema.tables "); + sb.append("WHERE table_catalog = ? "); + sb.append("AND table_schema = ? "); + sb.append("AND table_type = 'BASE TABLE'"); + + List params = new java.util.ArrayList<>(); + params.add(databaseName); + params.add(actualSchemaName); + + if (StringUtils.isNotBlank(tableNameLike)) { + sb.append(" AND table_name LIKE ?"); + params.add(tableNameLike); + } + + List tableNames; + try { + tableNames = jdbcOperations.query(sb.toString(), params.toArray(), + (rs, rowNum) -> rs.getString(1)); + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + return Collections.emptyList(); + } + throw e; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + return tableNames; + } + + @Override + public List listTables(String schemaName, String tableNameLike) { + List tableNames = showTablesLike(schemaName, tableNameLike); + return tableNames.stream().map(tableName -> { + DBObjectIdentity identity = new DBObjectIdentity(); + identity.setSchemaName(schemaName); + identity.setName(tableName); + return identity; + }).collect(Collectors.toList()); + } + + @Override + public List showExternalTablesLike(String schemaName, String tableNameLike) { + return Collections.emptyList(); + } + + @Override + public List listExternalTables(String schemaName, String tableNameLike) { + return Collections.emptyList(); + } + + @Override + public boolean isExternalTable(String schemaName, String tableName) { + return false; + } + + @Override + public boolean syncExternalTableFiles(String schemaName, String tableName) { + return false; + } + + @Override + public List listViews(String schemaName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + String sql = "SELECT table_name FROM information_schema.views " + + "WHERE table_catalog = ? AND table_schema = ?"; + try { + return jdbcOperations.query(sql, new Object[] {databaseName, actualSchemaName}, (rs, rowNum) -> { + DBObjectIdentity identity = new DBObjectIdentity(); + identity.setSchemaName(schemaName); + identity.setName(rs.getString("table_name")); + return identity; + }); + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + return Collections.emptyList(); + } + throw e; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public List listAllViews(String viewNameLike) { + return Collections.emptyList(); + } + + @Override + public List listAllUserViews(String viewNameLike) { + return Collections.emptyList(); + } + + @Override + public List listAllSystemViews(String viewNameLike) { + return Collections.emptyList(); + } + + @Override + public List showSystemViews(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listMViews(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listAllMViewsLike(String mViewNameLike) { + return Collections.emptyList(); + } + + @Override + public Boolean refreshMVData(DBMViewRefreshParameter parameter) { + return null; + } + + @Override + public DBMaterializedView getMView(String schemaName, String mViewName) { + return null; + } + + @Override + public List listMViewConstraints(String schemaName, String mViewName) { + return Collections.emptyList(); + } + + @Override + public List listMViewRefreshRecords(DBMViewRefreshRecordParam param) { + return Collections.emptyList(); + } + + @Override + public List listMViewIndexes(String schemaName, String mViewName) { + return Collections.emptyList(); + } + + @Override + public List showVariables() { + return Collections.emptyList(); + } + + @Override + public List showSessionVariables() { + return Collections.emptyList(); + } + + @Override + public List showGlobalVariables() { + return Collections.emptyList(); + } + + @Override + public List showCharset() { + // SQL Server 中字符集信息可以从 collation 中提取 + String sql = "SELECT DISTINCT " + + " SUBSTRING(collation_name, 1, CHARINDEX('_', collation_name) - 1) AS charset " + + "FROM sys.fn_helpcollations() " + + "WHERE SUBSTRING(collation_name, 1, CHARINDEX('_', collation_name) - 1) IS NOT NULL"; + try { + return jdbcOperations.queryForList(sql, String.class); + } catch (Exception e) { + log.warn("Failed to query charset", e); + return Collections.emptyList(); + } + } + + @Override + public List showCollation() { + String sql = "SELECT name FROM sys.fn_helpcollations() ORDER BY name"; + try { + return jdbcOperations.queryForList(sql, String.class); + } catch (Exception e) { + log.warn("Failed to query collation", e); + return Collections.emptyList(); + } + } + + @Override + public List listFunctions(String schemaName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + String sql = "SELECT ROUTINE_NAME as name, ROUTINE_SCHEMA as schema_name, ROUTINE_TYPE as type " + + "FROM information_schema.routines " + + "WHERE ROUTINE_CATALOG = ? AND ROUTINE_SCHEMA = ? " + + "AND ROUTINE_TYPE = 'FUNCTION' " + + "ORDER BY ROUTINE_NAME ASC"; + try { + return jdbcOperations.query(sql, new Object[] {databaseName, actualSchemaName}, (rs, rowNum) -> { + DBPLObjectIdentity identity = new DBPLObjectIdentity(); + identity.setSchemaName(schemaName); + identity.setName(rs.getString("name")); + identity.setType(DBObjectType.valueOf(rs.getString("type"))); + return identity; + }); + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + return Collections.emptyList(); + } + throw e; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public List listProcedures(String schemaName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + String sql = "SELECT ROUTINE_NAME as name, ROUTINE_SCHEMA as schema_name, ROUTINE_TYPE as type " + + "FROM information_schema.routines " + + "WHERE ROUTINE_CATALOG = ? AND ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' " + + "ORDER BY ROUTINE_NAME ASC"; + try { + return jdbcOperations.query(sql, new Object[] {databaseName, actualSchemaName}, (rs, rowNum) -> { + DBPLObjectIdentity identity = new DBPLObjectIdentity(); + identity.setSchemaName(schemaName); + identity.setName(rs.getString("name")); + identity.setType(DBObjectType.valueOf(rs.getString("type"))); + return identity; + }); + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + return Collections.emptyList(); + } + throw e; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public List listPackages(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listPackageBodies(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listTriggers(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listTypes(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listSequences(String schemaName) { + return Collections.emptyList(); + } + + @Override + public List listSynonyms(String schemaName, DBSynonymType synonymType) { + return Collections.emptyList(); + } + + @Override + public Map> listTableColumns(String schemaName, List tableNames) { + return Collections.emptyMap(); + } + + @Override + public List listTableColumns(String schemaName, String tableName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + String sql = "SELECT " + + " c.column_id AS ordinal_position, " + + " c.name AS column_name, " + + " t.name AS data_type, " + + " CASE " + + " WHEN t.name IN ('varchar', 'nvarchar', 'char', 'nchar', 'binary', 'varbinary') " + + " THEN t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR) END + ')' " + + " WHEN t.name IN ('decimal', 'numeric') " + + " THEN t.name + '(' + CAST(c.precision AS VARCHAR) + ',' + CAST(c.scale AS VARCHAR) + ')' " + + " WHEN t.name IN ('float', 'real') " + + " THEN t.name + '(' + CAST(c.precision AS VARCHAR) + ')' " + + " WHEN t.name IN ('datetime2', 'time', 'datetimeoffset') " + + " THEN t.name + '(' + CAST(c.scale AS VARCHAR) + ')' " + + " ELSE t.name " + + " END AS full_type_name, " + + " c.precision, " + + " c.scale, " + + " c.max_length AS character_maximum_length, " + + " c.is_nullable, " + + " ISNULL(dc.definition, '') AS column_default, " + + " ISNULL(ep.value, '') AS column_comment " + + "FROM sys.columns c " + + "INNER JOIN sys.types t ON c.user_type_id = t.user_type_id " + + "INNER JOIN sys.tables tb ON c.object_id = tb.object_id " + + "INNER JOIN sys.schemas s ON tb.schema_id = s.schema_id " + + "LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id " + + "LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id " + + " AND ep.minor_id = c.column_id " + + " AND ep.name = 'MS_Description' " + + "WHERE DB_NAME() = ? " + + " AND tb.name = ? " + + " AND s.name = ? " // 使用参数而不是硬编码 'dbo' + + "ORDER BY c.column_id"; + + try { + return jdbcOperations.query(sql, new Object[] {databaseName, tableName, actualSchemaName}, (rs, rowNum) -> { + DBTableColumn column = new DBTableColumn(); + column.setSchemaName(schemaName); + column.setTableName(tableName); + column.setOrdinalPosition(rs.getInt("ordinal_position")); + column.setName(rs.getString("column_name")); + column.setTypeName(rs.getString("data_type")); + column.setFullTypeName(rs.getString("full_type_name")); + + Object precisionObj = rs.getObject("precision"); + if (precisionObj != null) { + column.setPrecision(rs.getLong("precision")); + } + + Object scaleObj = rs.getObject("scale"); + if (scaleObj != null) { + column.setScale(rs.getInt("scale")); + } + + Object maxLengthObj = rs.getObject("character_maximum_length"); + if (maxLengthObj != null) { + int maxLength = rs.getInt("character_maximum_length"); + if (maxLength > 0 && maxLength != -1) { + column.setMaxLength((long) maxLength); + } + } + + column.setNullable(rs.getBoolean("is_nullable")); + + String defaultValue = rs.getString("column_default"); + if (StringUtils.isNotBlank(defaultValue)) { + // SQL Server 默认值可能包含括号,需要清理 + defaultValue = defaultValue.trim(); + if (defaultValue.startsWith("(") && defaultValue.endsWith(")")) { + defaultValue = defaultValue.substring(1, defaultValue.length() - 1); + } + column.fillDefaultValue(defaultValue); + } + + String comment = rs.getString("column_comment"); + if (StringUtils.isNotBlank(comment)) { + column.setComment(comment); + } + + return column; + }); + } catch (Exception e) { + log.warn("Failed to list table columns for table: " + schemaName + "." + tableName, e); + return Collections.emptyList(); + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public Map> listBasicTableColumns(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public List listBasicTableColumns(String schemaName, String tableName) { + return Collections.emptyList(); + } + + @Override + public Map> listBasicViewColumns(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public List listBasicViewColumns(String schemaName, String viewName) { + return Collections.emptyList(); + } + + @Override + public Map> listBasicExternalTableColumns(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public List listBasicExternalTableColumns(String schemaName, String externalTableName) { + return Collections.emptyList(); + } + + @Override + public Map> listBasicMViewColumns(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public List listBasicMViewColumns(String schemaName, String externalTableName) { + return Collections.emptyList(); + } + + @Override + public Map> listBasicColumnsInfo(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public Map> listTableIndexes(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public Map> listTableConstraints(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public Map listTableOptions(String schemaName) { + return Collections.emptyMap(); + } + + @Override + public Map listTablePartitions(@NonNull String schemaName, List tableNames) { + return Collections.emptyMap(); + } + + @Override + public List listTableRangePartitionInfo(String tenantName) { + return Collections.emptyList(); + } + + @Override + public List listSubpartitions(String schemaName, String tableName) { + return Collections.emptyList(); + } + + @Override + public Boolean isLowerCaseTableName() { + // SQL Server 默认情况下表名是大小写不敏感的(取决于 collation) + // 但可以通过查询系统视图确认 + String sql = "SELECT CASE " + + " WHEN collation_name LIKE '%_CI%' THEN 1 " + + " ELSE 0 " + + "END AS is_case_insensitive " + + "FROM sys.databases " + + "WHERE name = DB_NAME()"; + try { + return jdbcOperations.query(sql, rs -> { + if (rs.next()) { + return rs.getInt(1) == 1; + } + return null; + }); + } catch (Exception e) { + log.warn("Failed to check case sensitivity", e); + return null; + } + } + + @Override + public List listPartitionTables(String partitionMethod) { + return Collections.emptyList(); + } + + @Override + public List listTableConstraints(String schemaName, String tableName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + // 查询主键和唯一约束 + String pkAndUniqueSql = "SELECT " + + " kc.name AS constraint_name, " + + " kc.type_desc AS constraint_type, " + + " c.name AS column_name, " + + " kc.is_system_named " + + "FROM sys.key_constraints kc " + + "INNER JOIN sys.tables t ON kc.parent_object_id = t.object_id " + + "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " + + "INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id AND kc.unique_index_id = ic.index_id " + + "INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id " + + "WHERE DB_NAME() = ? " + + " AND t.name = ? " + + " AND s.name = ? " // 使用参数 + + "ORDER BY kc.name, ic.key_ordinal"; + + // 查询外键约束 + String fkSql = "SELECT " + + " fk.name AS constraint_name, " + + " c.name AS column_name, " + + " OBJECT_SCHEMA_NAME(fk.referenced_object_id) AS referenced_schema_name, " + + " OBJECT_NAME(fk.referenced_object_id) AS referenced_table_name, " + + " rc.name AS referenced_column_name " + + "FROM sys.foreign_keys fk " + + "INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id " + + "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " + + "INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id " + + "INNER JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id " + + "INNER JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id " + + "WHERE DB_NAME() = ? " + + " AND t.name = ? " + + " AND s.name = ? " // 使用参数 + + "ORDER BY fk.name, fkc.constraint_column_id"; + + // 查询检查约束 + String checkSql = "SELECT " + + " cc.name AS constraint_name, " + + " cc.definition AS check_definition " + + "FROM sys.check_constraints cc " + + "INNER JOIN sys.tables t ON cc.parent_object_id = t.object_id " + + "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " + + "WHERE DB_NAME() = ? " + + " AND t.name = ? " + + " AND s.name = ?"; // 使用参数 + + try { + Map constraintMap = new java.util.LinkedHashMap<>(); + + // 处理主键和唯一约束 + jdbcOperations.query(pkAndUniqueSql, new Object[] {databaseName, tableName, actualSchemaName}, + (rs, rowNum) -> { + String constraintName = rs.getString("constraint_name"); + String constraintType = rs.getString("constraint_type"); + String columnName = rs.getString("column_name"); + + DBTableConstraint constraint = constraintMap.get(constraintName); + if (constraint == null) { + constraint = new DBTableConstraint(); + constraint.setName(constraintName); + constraint.setSchemaName(schemaName); + constraint.setTableName(tableName); + constraint.setOwner(schemaName); + + if ("PRIMARY_KEY_CONSTRAINT".equalsIgnoreCase(constraintType)) { + constraint.setType(DBConstraintType.PRIMARY_KEY); + } else if ("UNIQUE_CONSTRAINT".equalsIgnoreCase(constraintType)) { + constraint.setType(DBConstraintType.UNIQUE); + } + + constraint.setColumnNames(new java.util.ArrayList<>()); + constraintMap.put(constraintName, constraint); + } + + constraint.getColumnNames().add(columnName); + return null; + }); + + // 处理外键约束 + jdbcOperations.query(fkSql, new Object[] {databaseName, tableName, actualSchemaName}, (rs, rowNum) -> { + String constraintName = rs.getString("constraint_name"); + String columnName = rs.getString("column_name"); + String referencedSchemaName = rs.getString("referenced_schema_name"); + String referencedTableName = rs.getString("referenced_table_name"); + String referencedColumnName = rs.getString("referenced_column_name"); + + DBTableConstraint constraint = constraintMap.get(constraintName); + if (constraint == null) { + constraint = new DBTableConstraint(); + constraint.setName(constraintName); + constraint.setSchemaName(schemaName); + constraint.setTableName(tableName); + constraint.setOwner(schemaName); + constraint.setType(DBConstraintType.FOREIGN_KEY); + constraint.setReferenceSchemaName(referencedSchemaName); + constraint.setReferenceTableName(referencedTableName); + constraint.setColumnNames(new java.util.ArrayList<>()); + constraint.setReferenceColumnNames(new java.util.ArrayList<>()); + constraintMap.put(constraintName, constraint); + } + + constraint.getColumnNames().add(columnName); + constraint.getReferenceColumnNames().add(referencedColumnName); + return null; + }); + + // 处理检查约束 + jdbcOperations.query(checkSql, new Object[] {databaseName, tableName, actualSchemaName}, (rs, rowNum) -> { + String constraintName = rs.getString("constraint_name"); + String checkDefinition = rs.getString("check_definition"); + + DBTableConstraint constraint = new DBTableConstraint(); + constraint.setName(constraintName); + constraint.setSchemaName(schemaName); + constraint.setTableName(tableName); + constraint.setOwner(schemaName); + constraint.setType(DBConstraintType.CHECK); + // constraint.setCheckExpression(checkDefinition); + constraint.setColumnNames(new java.util.ArrayList<>()); + constraintMap.put(constraintName, constraint); + return null; + }); + + return new java.util.ArrayList<>(constraintMap.values()); + } catch (Exception e) { + log.warn("Failed to list table constraints for table: " + schemaName + "." + tableName, e); + return Collections.emptyList(); + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public DBTablePartition getPartition(String schemaName, String tableName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return null; + } + + // SQL Server 支持表分区,查询分区信息 + String sql = "SELECT " + + " ps.name AS partition_scheme_name, " + + " pf.name AS partition_function_name, " + + " pf.type_desc AS partition_function_type, " + + " p.partition_number, " + + " p.rows AS partition_rows " + + "FROM sys.tables t " + + "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " + + "INNER JOIN sys.indexes i ON t.object_id = i.object_id AND i.type IN (0, 1) " + + "LEFT JOIN sys.partition_schemes ps ON i.data_space_id = ps.data_space_id " + + "LEFT JOIN sys.partition_functions pf ON ps.function_id = pf.function_id " + + "LEFT JOIN sys.partitions p ON t.object_id = p.object_id AND i.index_id = p.index_id " + + "WHERE DB_NAME() = ? " + + " AND t.name = ? " + + " AND s.name = ? " // 使用参数 + + "ORDER BY p.partition_number"; + + try { + DBTablePartition partition = new DBTablePartition(); + AtomicReference partitionSchemeName = new AtomicReference<>(); + AtomicReference partitionFunctionName = new AtomicReference<>(); + AtomicReference partitionFunctionType = new AtomicReference<>(); + java.util.List definitions = new java.util.ArrayList<>(); + + jdbcOperations.query(sql, new Object[] {databaseName, tableName, actualSchemaName}, rs -> { + if (partitionSchemeName.get() == null) { + partitionSchemeName.set(rs.getString("partition_scheme_name")); + partitionFunctionName.set(rs.getString("partition_function_name")); + partitionFunctionType.set(rs.getString("partition_function_type")); + + if (partitionSchemeName.get() != null) { + DBTablePartitionOption option = new DBTablePartitionOption(); + // option.setMethod(partitionFunctionType.get()); + option.setExpression(partitionFunctionName.get()); + partition.setPartitionOption(option); + } + } + + if (partitionSchemeName.get() != null) { + DBTablePartitionDefinition definition = new DBTablePartitionDefinition(); + definition.setName("Partition_" + rs.getInt("partition_number")); + definition.setOrdinalPosition(rs.getInt("partition_number")); + // definition.setRowCount(rs.getLong("partition_rows")); + definitions.add(definition); + } + }); + + if (!definitions.isEmpty()) { + partition.setPartitionDefinitions(definitions); + } else { + return null; // 表未分区 + } + + return partition; + } catch (Exception e) { + log.warn("Failed to get partition for table: " + schemaName + "." + tableName, e); + return null; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public List listTableIndexes(String schemaName, String tableName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return Collections.emptyList(); + } + + String sql = "SELECT " + + " i.name AS index_name, " + + " i.type_desc AS index_type, " + + " i.is_unique, " + + " i.is_primary_key, " + + " ic.key_ordinal AS ordinal_position, " + + " c.name AS column_name, " + + " ic.is_descending_key, " + + " i.is_disabled, " + + " i.filter_definition " + + "FROM sys.indexes i " + + "INNER JOIN sys.tables t ON i.object_id = t.object_id " + + "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " + + "INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id " + + "INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id " + + "WHERE DB_NAME() = ? " + + " AND t.name = ? " + + " AND s.name = ? " // 使用参数而不是硬编码 'dbo' + + " AND i.type > 0 " // 排除堆(heap) + + "ORDER BY i.name, ic.key_ordinal"; + + try { + Map indexMap = new java.util.LinkedHashMap<>(); + jdbcOperations.query(sql, new Object[] {databaseName, tableName, actualSchemaName}, (rs, rowNum) -> { + String indexName = rs.getString("index_name"); + DBTableIndex index = indexMap.get(indexName); + + if (index == null) { + index = new DBTableIndex(); + index.setSchemaName(schemaName); + index.setTableName(tableName); + index.setName(indexName); + index.setOrdinalPosition(rs.getInt("ordinal_position")); + index.setPrimary(rs.getBoolean("is_primary_key")); + index.setNonUnique(!rs.getBoolean("is_unique")); + // index.setDisabled(rs.getBoolean("is_disabled")); + + String indexType = rs.getString("index_type"); + if ("CLUSTERED".equalsIgnoreCase(indexType)) { + index.setType(DBIndexType.CLUSTERED); + } else if ("NONCLUSTERED".equalsIgnoreCase(indexType)) { + if (index.isNonUnique()) { + index.setType(DBIndexType.NORMAL); + } else { + index.setType(DBIndexType.UNIQUE); + } + } else { + index.setType(DBIndexType.NORMAL); + } + + String filterDefinition = rs.getString("filter_definition"); + if (StringUtils.isNotBlank(filterDefinition)) { + index.setAdditionalInfo("Filter: " + filterDefinition); + } + + index.setColumnNames(new java.util.ArrayList<>()); + indexMap.put(indexName, index); + } + + String columnName = rs.getString("column_name"); + boolean isDescending = rs.getBoolean("is_descending_key"); + if (isDescending) { + columnName = columnName + " DESC"; + } + index.getColumnNames().add(columnName); + + return null; + }); + + return new java.util.ArrayList<>(indexMap.values()); + } catch (Exception e) { + log.warn("Failed to list table indexes for table: " + schemaName + "." + tableName, e); + return Collections.emptyList(); + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public String getTableDDL(String schemaName, String tableName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // SQL Server 可以使用 OBJECT_DEFINITION 或者查询系统视图来生成 DDL + // 但更准确的方法是使用系统存储过程 sp_helptext 或者查询 sys.sql_modules + // 对于表,我们需要手动构建 DDL,因为 SQL Server 没有直接提供表的 DDL 函数 + // 这里我们使用一个简化的方法,通过查询系统视图来生成基本的 CREATE TABLE 语句 + + try { + // 获取列信息(listTableColumns 内部会处理数据库切换) + List columns = listTableColumns(schemaName, tableName); + if (columns.isEmpty()) { + return ""; + } + + StringBuilder ddl = new StringBuilder(); + ddl.append("CREATE TABLE [").append(tableName).append("] (\n"); + + // 添加列定义 + for (int i = 0; i < columns.size(); i++) { + DBTableColumn column = columns.get(i); + if (i > 0) { + ddl.append(",\n"); + } + ddl.append(" [").append(column.getName()).append("] "); + ddl.append(column.getFullTypeName()); + + if (!column.getNullable()) { + ddl.append(" NOT NULL"); + } + + if (column.getDefaultValue() != null && StringUtils.isNotBlank(column.getDefaultValue().toString())) { + ddl.append(" DEFAULT ").append(column.getDefaultValue()); + } + } + + // 添加主键约束 + List constraints = listTableConstraints(schemaName, tableName); + for (DBTableConstraint constraint : constraints) { + if (constraint.getType() == DBConstraintType.PRIMARY_KEY) { + ddl.append(",\n CONSTRAINT [").append(constraint.getName()).append("] PRIMARY KEY ("); + for (int i = 0; i < constraint.getColumnNames().size(); i++) { + if (i > 0) { + ddl.append(", "); + } + ddl.append("[").append(constraint.getColumnNames().get(i)).append("]"); + } + ddl.append(")"); + } + } + + ddl.append("\n);"); + + return ddl.toString(); + } catch (Exception e) { + log.warn("Failed to get table DDL for table: " + schemaName + "." + tableName, e); + return ""; + } + } + + @Override + public DBTableOptions getTableOptions(String schemaName, String tableName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return new DBTableOptions(); + } + + String sql = "SELECT " + + " t.create_date AS create_time, " + + " t.modify_date AS update_time, " + + " t.name AS table_name, " + + " ISNULL(ep.value, '') AS table_comment " + + "FROM sys.tables t " + + "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " + + "LEFT JOIN sys.extended_properties ep ON ep.major_id = t.object_id " + + " AND ep.minor_id = 0 " + + " AND ep.name = 'MS_Description' " + + "WHERE DB_NAME() = ? " + + " AND t.name = ? " + + " AND s.name = ?"; // 使用参数 + + try { + DBTableOptions options = new DBTableOptions(); + jdbcOperations.query(sql, new Object[] {databaseName, tableName, actualSchemaName}, rs -> { + if (rs.next()) { + java.sql.Timestamp createTime = rs.getTimestamp("create_time"); + if (createTime != null) { + options.setCreateTime(createTime); + } + + java.sql.Timestamp updateTime = rs.getTimestamp("update_time"); + if (updateTime != null) { + options.setUpdateTime(updateTime); + } + + String comment = rs.getString("table_comment"); + if (StringUtils.isNotBlank(comment)) { + options.setComment(comment); + } + } + }); + return options; + } catch (Exception e) { + log.warn("Failed to get table options for table: " + schemaName + "." + tableName, e); + return new DBTableOptions(); + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public DBTableOptions getTableOptions(String schemaName, String tableName, String ddl) { + return null; + } + + @Override + public List listTableColumnGroups(String schemaName, String tableName) { + return Collections.emptyList(); + } + + @Override + public DBView getView(String schemaName, String viewName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return null; + } + + DBView view = new DBView(); + view.setViewName(viewName); + view.setSchemaName(schemaName); + + try { + // 查询视图基本信息 + String infoSql = "SELECT " + + " TABLE_SCHEMA, " + + " CHECK_OPTION, " + + " IS_UPDATABLE " + + "FROM information_schema.views " + + "WHERE TABLE_CATALOG = ? " + + " AND TABLE_SCHEMA = ? " + + " AND TABLE_NAME = ?"; + + jdbcOperations.query(infoSql, new Object[] {databaseName, actualSchemaName, viewName}, rs -> { + view.setDefiner(rs.getString("TABLE_SCHEMA")); + String checkOption = rs.getString("CHECK_OPTION"); + if (StringUtils.isNotBlank(checkOption)) { + if ("CASCADE".equalsIgnoreCase(checkOption)) { + // SQL Server 不支持 CASCADE,但可以设置为 NONE + view.setCheckOption(DBViewCheckOption.NONE.name()); + } else { + view.setCheckOption(DBViewCheckOption.NONE.name()); + } + } else { + view.setCheckOption(DBViewCheckOption.NONE.name()); + } + String isUpdatable = rs.getString("IS_UPDATABLE"); + view.setUpdatable("YES".equalsIgnoreCase(isUpdatable)); + }); + + // 获取视图的 DDL - 使用 OBJECT_DEFINITION 函数 + // 由于已经切换到正确的数据库上下文,可以直接使用 schema.object 格式 + // OBJECT_ID 函数需要字符串参数,格式为 'schema.object',不需要方括号 + String objectName = actualSchemaName + "." + viewName; + String ddlSql = "SELECT OBJECT_DEFINITION(OBJECT_ID('" + objectName + "')) AS view_definition"; + AtomicReference viewDefinition = new AtomicReference<>(); + jdbcOperations.query(ddlSql, rs -> { + String definition = rs.getString("view_definition"); + if (StringUtils.isNotBlank(definition)) { + viewDefinition.set(definition); + } + }); + + // 如果 OBJECT_DEFINITION 返回空,尝试使用 sys.sql_modules + if (StringUtils.isBlank(viewDefinition.get())) { + String moduleSql = "SELECT m.definition " + + "FROM sys.sql_modules m " + + "INNER JOIN sys.views v ON m.object_id = v.object_id " + + "INNER JOIN sys.schemas s ON v.schema_id = s.schema_id " + + "WHERE DB_NAME() = ? " + + " AND v.name = ? " + + " AND s.name = ?"; + jdbcOperations.query(moduleSql, new Object[] {databaseName, viewName, actualSchemaName}, rs -> { + viewDefinition.set(rs.getString("definition")); + }); + } + + // 构建 DDL + if (StringUtils.isNotBlank(viewDefinition.get())) { + StringBuilder ddl = new StringBuilder(); + ddl.append("CREATE VIEW "); + if (StringUtils.isNotEmpty(actualSchemaName)) { + ddl.append("[").append(actualSchemaName).append("]."); + } + ddl.append("[").append(viewName).append("]"); + if (view.getCheckOption() != null && view.getCheckOption() != DBViewCheckOption.NONE) { + ddl.append(" WITH ").append(view.getCheckOption().name()); + } + ddl.append(" AS ").append(viewDefinition.get()); + view.setDdl(ddl.toString()); + } + + // 获取视图的列信息 + String columnSql = "SELECT " + + " c.column_id AS ordinal_position, " + + " c.name AS column_name, " + + " t.name AS data_type, " + + " CASE " + + " WHEN t.name IN ('varchar', 'nvarchar', 'char', 'nchar', 'binary', 'varbinary') " + + " THEN t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR) END + ')' " + + " WHEN t.name IN ('decimal', 'numeric') " + + " THEN t.name + '(' + CAST(c.precision AS VARCHAR) + ',' + CAST(c.scale AS VARCHAR) + ')' " + + " WHEN t.name IN ('float', 'real') " + + " THEN t.name + '(' + CAST(c.precision AS VARCHAR) + ')' " + + " WHEN t.name IN ('datetime2', 'time', 'datetimeoffset') " + + " THEN t.name + '(' + CAST(c.scale AS VARCHAR) + ')' " + + " ELSE t.name " + + " END AS full_type_name, " + + " c.is_nullable, " + + " ISNULL(ep.value, '') AS column_comment " + + "FROM sys.columns c " + + "INNER JOIN sys.types t ON c.user_type_id = t.user_type_id " + + "INNER JOIN sys.views v ON c.object_id = v.object_id " + + "INNER JOIN sys.schemas s ON v.schema_id = s.schema_id " + + "LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id " + + " AND ep.minor_id = c.column_id " + + " AND ep.name = 'MS_Description' " + + "WHERE DB_NAME() = ? " + + " AND v.name = ? " + + " AND s.name = ? " + + "ORDER BY c.column_id"; + + List columns = jdbcOperations.query(columnSql, + new Object[] {databaseName, viewName, actualSchemaName}, (rs, rowNum) -> { + DBTableColumn column = new DBTableColumn(); + column.setOrdinalPosition(rs.getInt("ordinal_position")); + column.setName(rs.getString("column_name")); + column.setTypeName(rs.getString("data_type")); + column.setFullTypeName(rs.getString("full_type_name")); + column.setNullable(rs.getBoolean("is_nullable")); + column.setComment(rs.getString("column_comment")); + column.setSchemaName(schemaName); + column.setTableName(viewName); + return column; + }); + view.setColumns(columns); + + return view; + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + log.warn("View not found: " + schemaName + "." + viewName); + return null; + } + throw e; + } catch (Exception e) { + log.warn("Failed to get view: " + schemaName + "." + viewName, e); + return null; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public DBFunction getFunction(String schemaName, String functionName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return null; + } + + DBFunction function = new DBFunction(); + function.setFunName(functionName); + // function.setSchemaName(schemaName); + + try { + // 查询函数基本信息 + String infoSql = "SELECT " + + " ROUTINE_SCHEMA, " + + " CREATED, " + + " LAST_ALTERED, " + + " ROUTINE_DEFINITION " + + "FROM information_schema.routines " + + "WHERE ROUTINE_CATALOG = ? " + + " AND ROUTINE_SCHEMA = ? " + + " AND ROUTINE_NAME = ?"; + + AtomicReference routineDefinition = new AtomicReference<>(); + jdbcOperations.query(infoSql, new Object[] {databaseName, actualSchemaName, functionName}, rs -> { + function.setDefiner(rs.getString("ROUTINE_SCHEMA")); + Timestamp created = rs.getTimestamp("CREATED"); + if (created != null) { + function.setCreateTime(created); + } + Timestamp lastAltered = rs.getTimestamp("LAST_ALTERED"); + if (lastAltered != null) { + function.setModifyTime(lastAltered); + } + routineDefinition.set(rs.getString("ROUTINE_DEFINITION")); + }); + + // 查询函数参数和返回类型 + String paramSql = "SELECT " + + " PARAMETER_MODE, " + + " PARAMETER_NAME, " + + " DATA_TYPE, " + + " CHARACTER_MAXIMUM_LENGTH, " + + " NUMERIC_PRECISION, " + + " NUMERIC_SCALE, " + + " ORDINAL_POSITION " + + "FROM information_schema.parameters " + + "WHERE SPECIFIC_CATALOG = ? " + + " AND SPECIFIC_SCHEMA = ? " + + " AND SPECIFIC_NAME = ? " + + "ORDER BY ORDINAL_POSITION"; + + List params = new ArrayList<>(); + AtomicReference returnType = new AtomicReference<>(); + + jdbcOperations.query(paramSql, new Object[] {databaseName, actualSchemaName, functionName}, rs -> { + String paramMode = rs.getString("PARAMETER_MODE"); + String paramName = rs.getString("PARAMETER_NAME"); + String dataType = rs.getString("DATA_TYPE"); + Integer maxLength = rs.getObject("CHARACTER_MAXIMUM_LENGTH", Integer.class); + Integer precision = rs.getObject("NUMERIC_PRECISION", Integer.class); + Integer scale = rs.getObject("NUMERIC_SCALE", Integer.class); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + + // 构建完整的数据类型字符串 + StringBuilder fullDataType = new StringBuilder(dataType); + if (maxLength != null && maxLength > 0) { + if (maxLength == -1) { + fullDataType.append("(MAX)"); + } else { + fullDataType.append("(").append(maxLength).append(")"); + } + } else if (precision != null && scale != null) { + fullDataType.append("(").append(precision).append(",").append(scale).append(")"); + } else if (precision != null) { + fullDataType.append("(").append(precision).append(")"); + } + + // 如果 PARAMETER_MODE 为 NULL,表示这是返回类型 + if (paramMode == null || "NULL".equalsIgnoreCase(paramMode)) { + returnType.set(fullDataType.toString()); + } else { + // 这是输入参数 + DBPLParam param = new DBPLParam(); + param.setParamName(paramName); + param.setSeqNum(ordinalPosition); + param.setDataType(fullDataType.toString()); + // SQL Server 函数参数通常是 IN 类型 + param.setParamMode(DBPLParamMode.IN); + params.add(param); + } + }); + + function.setReturnType(returnType.get()); + function.setParams(params); + + // 构建 DDL + StringBuilder ddl = new StringBuilder(); + ddl.append("CREATE FUNCTION "); + if (StringUtils.isNotEmpty(actualSchemaName)) { + ddl.append("[").append(actualSchemaName).append("]."); + } + ddl.append("[").append(functionName).append("]"); + ddl.append("("); + + // 添加参数列表 + if (!params.isEmpty()) { + for (int i = 0; i < params.size(); i++) { + DBPLParam param = params.get(i); + if (i > 0) { + ddl.append(", "); + } + ddl.append("@").append(param.getParamName()).append(" ").append(param.getDataType()); + } + } + ddl.append(")"); + ddl.append(" RETURNS ").append(returnType.get()); + ddl.append(" AS BEGIN "); + if (StringUtils.isNotBlank(routineDefinition.get())) { + ddl.append(routineDefinition.get()); + } + ddl.append(" END"); + + function.setDdl(ddl.toString()); + + return function; + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + log.warn("Function not found: " + schemaName + "." + functionName); + return null; + } + throw e; + } catch (Exception e) { + log.warn("Failed to get function: " + schemaName + "." + functionName, e); + return null; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public DBProcedure getProcedure(String schemaName, String procedureName) { + // 解析 database.schema 格式 + String[] dbAndSchema = parseDatabaseAndSchema(schemaName); + String databaseName = dbAndSchema[0]; + String actualSchemaName = dbAndSchema[1]; + + // 确保在正确的数据库中查询 + String currentDb = null; + try { + currentDb = jdbcOperations.queryForObject("SELECT DB_NAME()", String.class); + if (!databaseName.equals(currentDb)) { + switchDatabase(databaseName); + } + } catch (Exception e) { + log.warn("Failed to switch to database: " + databaseName, e); + return null; + } + + DBProcedure procedure = new DBProcedure(); + procedure.setProName(procedureName); + + try { + // 查询存储过程基本信息 + String infoSql = "SELECT " + + " ROUTINE_SCHEMA, " + + " CREATED, " + + " LAST_ALTERED, " + + " ROUTINE_DEFINITION " + + "FROM information_schema.routines " + + "WHERE ROUTINE_CATALOG = ? " + + " AND ROUTINE_SCHEMA = ? " + + " AND ROUTINE_TYPE = 'PROCEDURE' " + + " AND ROUTINE_NAME = ?"; + + AtomicReference routineDefinition = new AtomicReference<>(); + jdbcOperations.query(infoSql, new Object[] {databaseName, actualSchemaName, procedureName}, rs -> { + procedure.setDefiner(rs.getString("ROUTINE_SCHEMA")); + Timestamp created = rs.getTimestamp("CREATED"); + if (created != null) { + procedure.setCreateTime(created); + } + Timestamp lastAltered = rs.getTimestamp("LAST_ALTERED"); + if (lastAltered != null) { + procedure.setModifyTime(lastAltered); + } + routineDefinition.set(rs.getString("ROUTINE_DEFINITION")); + }); + + // 查询存储过程参数 + String paramSql = "SELECT " + + " PARAMETER_MODE, " + + " PARAMETER_NAME, " + + " DATA_TYPE, " + + " CHARACTER_MAXIMUM_LENGTH, " + + " NUMERIC_PRECISION, " + + " NUMERIC_SCALE, " + + " ORDINAL_POSITION " + + "FROM information_schema.parameters " + + "WHERE SPECIFIC_CATALOG = ? " + + " AND SPECIFIC_SCHEMA = ? " + + " AND SPECIFIC_NAME = ? " + + "ORDER BY ORDINAL_POSITION"; + + List params = new ArrayList<>(); + + jdbcOperations.query(paramSql, new Object[] {databaseName, actualSchemaName, procedureName}, rs -> { + String paramMode = rs.getString("PARAMETER_MODE"); + String paramName = rs.getString("PARAMETER_NAME"); + String dataType = rs.getString("DATA_TYPE"); + Integer maxLength = rs.getObject("CHARACTER_MAXIMUM_LENGTH", Integer.class); + Integer precision = rs.getObject("NUMERIC_PRECISION", Integer.class); + Integer scale = rs.getObject("NUMERIC_SCALE", Integer.class); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + + // 构建完整的数据类型字符串 + StringBuilder fullDataType = new StringBuilder(dataType); + if (maxLength != null && maxLength > 0) { + if (maxLength == -1) { + fullDataType.append("(MAX)"); + } else { + fullDataType.append("(").append(maxLength).append(")"); + } + } else if (precision != null && scale != null) { + fullDataType.append("(").append(precision).append(",").append(scale).append(")"); + } else if (precision != null) { + fullDataType.append("(").append(precision).append(")"); + } + + // 存储过程参数处理 + DBPLParam param = new DBPLParam(); + param.setParamName(paramName); + param.setSeqNum(ordinalPosition); + param.setDataType(fullDataType.toString()); + + // SQL Server 存储过程参数模式映射 + // IN - 输入参数(默认) + // OUT - 输出参数(SQL Server 使用 OUTPUT 关键字) + // INOUT - 输入输出参数 + if (paramMode == null || StringUtils.isBlank(paramMode)) { + // 默认为输入参数 + param.setParamMode(DBPLParamMode.IN); + } else if ("IN".equalsIgnoreCase(paramMode)) { + param.setParamMode(DBPLParamMode.IN); + } else if ("OUT".equalsIgnoreCase(paramMode)) { + param.setParamMode(DBPLParamMode.OUT); + } else if ("INOUT".equalsIgnoreCase(paramMode)) { + param.setParamMode(DBPLParamMode.INOUT); + } else { + param.setParamMode(DBPLParamMode.UNKNOWN); + } + + params.add(param); + }); + + procedure.setParams(params); + + // 构建 DDL + StringBuilder ddl = new StringBuilder(); + ddl.append("CREATE PROCEDURE "); + if (StringUtils.isNotEmpty(actualSchemaName)) { + ddl.append("[").append(actualSchemaName).append("]."); + } + ddl.append("[").append(procedureName).append("]"); + ddl.append("("); + + // 添加参数列表 + if (!params.isEmpty()) { + for (int i = 0; i < params.size(); i++) { + DBPLParam param = params.get(i); + if (i > 0) { + ddl.append(", "); + } + ddl.append("@").append(param.getParamName()).append(" ").append(param.getDataType()); + // SQL Server 存储过程输出参数使用 OUTPUT 关键字 + if (param.getParamMode() == DBPLParamMode.OUT || param.getParamMode() == DBPLParamMode.INOUT) { + ddl.append(" OUTPUT"); + } + } + } + ddl.append(")"); + ddl.append(" AS BEGIN "); + if (StringUtils.isNotBlank(routineDefinition.get())) { + ddl.append(routineDefinition.get()); + } + ddl.append(" END"); + + procedure.setDdl(ddl.toString()); + + return procedure; + } catch (BadSqlGrammarException e) { + if (StringUtils.containsIgnoreCase(e.getMessage(), "Invalid object name") || + StringUtils.containsIgnoreCase(e.getMessage(), "Invalid schema")) { + log.warn("Procedure not found: " + schemaName + "." + procedureName); + return null; + } + throw e; + } catch (Exception e) { + log.warn("Failed to get procedure: " + schemaName + "." + procedureName, e); + return null; + } finally { + // 恢复原数据库上下文 + if (currentDb != null && !currentDb.equals(databaseName)) { + try { + switchDatabase(currentDb); + } catch (Exception e) { + log.warn("Failed to restore database context", e); + } + } + } + } + + @Override + public DBPackage getPackage(String schemaName, String packageName) { + return null; + } + + @Override + public DBTrigger getTrigger(String schemaName, String packageName) { + return null; + } + + @Override + public DBType getType(String schemaName, String typeName) { + return null; + } + + @Override + public DBSequence getSequence(String schemaName, String sequenceName) { + return null; + } + + @Override + public DBSynonym getSynonym(String schemaName, String synonymName, DBSynonymType synonymType) { + return null; + } + + @Override + public Map getTables(String schemaName, List tableNames) { + return Collections.emptyMap(); + } +} diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java index 53dfc82746..f46ea39ac9 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/stats/DBStatsAccessorFactory.java @@ -111,6 +111,11 @@ public DBStatsAccessor buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBStatsAccessor buildForSqlServer() { + return null; + } + private JdbcOperations getJdbcOperations() { if (this.jdbcOperations != null) { return this.jdbcOperations; diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java index 443e5e4ba5..7940b3853d 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBFunctionTemplateFactory.java @@ -57,4 +57,9 @@ public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBMViewTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBMViewTemplateFactory.java index ed752cdd99..ae9381c22f 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBMViewTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBMViewTemplateFactory.java @@ -62,4 +62,9 @@ public DBObjectTemplate buildForOdpSharding() { public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("not support yet"); } + + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("not support yet"); + } } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBPackageTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBPackageTemplateFactory.java index 386de9e76a..77752e77b2 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBPackageTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBPackageTemplateFactory.java @@ -69,6 +69,11 @@ public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + private JdbcOperations getJdbcOperations() { if (this.jdbcOperations != null) { return this.jdbcOperations; diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java index 54854614b6..4ca08a0e9d 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBProcedureTemplateFactory.java @@ -57,4 +57,9 @@ public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTriggerTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTriggerTemplateFactory.java index a6b9cb3bb2..d7500b6971 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTriggerTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTriggerTemplateFactory.java @@ -56,4 +56,9 @@ public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTypeTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTypeTemplateFactory.java index c64e3dd69b..1abbc67d41 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTypeTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBTypeTemplateFactory.java @@ -56,4 +56,9 @@ public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java index 23e7652b87..41ed3cb891 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/template/DBViewTemplateFactory.java @@ -57,4 +57,9 @@ public DBObjectTemplate buildForPostgres() { throw new UnsupportedOperationException("Not supported yet"); } + @Override + public DBObjectTemplate buildForSqlServer() { + throw new UnsupportedOperationException("Not supported yet"); + } + } diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/SqlServerSqlBuilder.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/SqlServerSqlBuilder.java new file mode 100644 index 0000000000..cde12b5518 --- /dev/null +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/SqlServerSqlBuilder.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.tools.dbbrowser.util; + +public class SqlServerSqlBuilder extends SqlBuilder { + + public SqlServerSqlBuilder() { + super(); + } + + @Override + public SqlBuilder identifier(String identifier) { + return append(StringUtils.quoteSqlServerIdentifier(identifier)); + } + + @Override + public SqlBuilder value(String value) { + return append(StringUtils.quoteSqlServerValue(value)); + } + + @Override + public SqlBuilder defaultValue(String value) { + // SQL Server default value handling similar to Oracle + return append(value); + } + + @Override + public SqlBuilder like(String fieldKey, String fieldLikeValue) { + // SQL Server LIKE clause uses backslash for escaping (similar to MySQL) + return super.like(fieldKey, fieldLikeValue); + } +} diff --git a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/StringUtils.java b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/StringUtils.java index 2fce5dff83..6e3bcc8fb0 100644 --- a/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/StringUtils.java +++ b/libs/db-browser/src/main/java/com/oceanbase/tools/dbbrowser/util/StringUtils.java @@ -29,6 +29,8 @@ public abstract class StringUtils extends org.apache.commons.lang3.StringUtils { private static final char MYSQL_IDENTIFIER_WRAP_CHAR = '`'; private static final char ORACLE_IDENTIFIER_WRAP_CHAR = '"'; + private static final char SQLSERVER_IDENTIFIER_LEFT_CHAR = '['; + private static final char SQLSERVER_IDENTIFIER_RIGHT_CHAR = ']'; private static final String DEFAULT_VARIABLE_SUFFIX = "}"; private static final String DEFAULT_VARIABLE_PREFIX = "${"; @@ -43,6 +45,20 @@ public static String quoteOracleIdentifier(final String str) { return quoteSqlIdentifier(str, ORACLE_IDENTIFIER_WRAP_CHAR); } + /** + * Quote SQL Server identifier using square brackets [identifier] + * Escape ] as ]] + */ + public static String quoteSqlServerIdentifier(final String str) { + if (null == str) { + return null; + } + // SQL Server uses [] for identifiers, escape ] as ]] + String escaped = replace(str, String.valueOf(SQLSERVER_IDENTIFIER_RIGHT_CHAR), + String.valueOf(SQLSERVER_IDENTIFIER_RIGHT_CHAR) + SQLSERVER_IDENTIFIER_RIGHT_CHAR); + return SQLSERVER_IDENTIFIER_LEFT_CHAR + escaped + SQLSERVER_IDENTIFIER_RIGHT_CHAR; + } + static String quoteSqlIdentifier(final String str, final char wrapChar) { if (null == str) { return null; @@ -78,6 +94,14 @@ public static String quoteMysqlValue(final String str) { return quoteSqlValue(str, '\'', new char[] {'\'', '\\'}); } + /** + * Quote SQL Server value using single quotes + * Escape ' as '' (similar to Oracle) + */ + public static String quoteSqlServerValue(final String str) { + return quoteSqlValue(str, '\''); + } + /** * for oracle mode */ diff --git a/pom.xml b/pom.xml index 85cd3f8351..a5a9344fe8 100644 --- a/pom.xml +++ b/pom.xml @@ -141,6 +141,7 @@ 8.0.30 21.1.0.0 42.7.3 + 12.8.2.jre8 1.8.0 2.7.11 @@ -157,6 +158,7 @@ 1.9.5 2.7.12 4.7.0 + true @@ -1213,6 +1215,11 @@ postgresql ${postgres.jdbc.version} + + com.microsoft.sqlserver + mssql-jdbc + ${mssql.jdbc.version} + com.oracle.database.nls orai18n diff --git a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ConnectType.java b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ConnectType.java index 809a18dbb3..e9c151a584 100644 --- a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ConnectType.java +++ b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/ConnectType.java @@ -31,6 +31,7 @@ public enum ConnectType { MYSQL(DialectType.MYSQL), DORIS(DialectType.DORIS), POSTGRESQL(DialectType.POSTGRESQL), + SQL_SERVER(DialectType.SQL_SERVER), // reserved for future version ODP_SHARDING_OB_ORACLE(DialectType.OB_ORACLE), diff --git a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/DialectType.java b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/DialectType.java index b83c369e90..7ef83706c0 100644 --- a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/DialectType.java +++ b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/DialectType.java @@ -29,6 +29,7 @@ public enum DialectType { ODP_SHARDING_OB_MYSQL, DORIS, POSTGRESQL, + SQL_SERVER, FILE_SYSTEM, UNKNOWN, ; @@ -68,4 +69,8 @@ public boolean isPostgreSql() { return POSTGRESQL == this; } + public boolean isSqlServer() { + return SQL_SERVER == this; + } + } diff --git a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/OdcConstants.java b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/OdcConstants.java index 0b646f83e2..6df42d031e 100644 --- a/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/OdcConstants.java +++ b/server/odc-core/src/main/java/com/oceanbase/odc/core/shared/constant/OdcConstants.java @@ -70,6 +70,7 @@ public class OdcConstants { public static final String MYSQL_DEFAULT_SCHEMA = "information_schema"; public static final String POSTGRESQL_DEFAULT_SCHEMA = "public"; + public static final String SQL_SERVER_DEFAULT_SCHEMA = "master"; public static final String ODC_BACK_URL_PARAM = "odc_back_url"; public static final String TEST_LOGIN_ID_PARAM = "test_login_id"; @@ -91,6 +92,10 @@ public class OdcConstants { * postgreSql driver class name */ public static final String POSTGRES_DRIVER_CLASS_NAME = "org.postgresql.Driver"; + /** + * SQL Server driver class name + */ + public static final String SQL_SERVER_DRIVER_CLASS_NAME = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; /** * Parameters name diff --git a/server/odc-core/src/main/java/com/oceanbase/odc/core/sql/split/SqlCommentProcessor.java b/server/odc-core/src/main/java/com/oceanbase/odc/core/sql/split/SqlCommentProcessor.java index aa6f90afaf..047bd3f147 100644 --- a/server/odc-core/src/main/java/com/oceanbase/odc/core/sql/split/SqlCommentProcessor.java +++ b/server/odc-core/src/main/java/com/oceanbase/odc/core/sql/split/SqlCommentProcessor.java @@ -167,6 +167,9 @@ public synchronized List split(StringBuffer buffer, String sqlScri addLineOracle(offsetStrings, buffer, bufferOrder, item); } else if (Objects.nonNull(this.dialectType) && this.dialectType.isDoris()) { addLineMysql(offsetStrings, buffer, bufferOrder, item); + } else if (Objects.nonNull(this.dialectType) && this.dialectType.isSqlServer()) { + // TODO: 这里暂时使用MySQL的逻辑,避免抛出异常 + addLineMysql(offsetStrings, buffer, bufferOrder, item); } else { throw new IllegalArgumentException("dialect type is illegal"); } diff --git a/server/odc-migrate/src/main/resources/migrate/common/R_2_0_0__initialize_version_diff_config.sql b/server/odc-migrate/src/main/resources/migrate/common/R_2_0_0__initialize_version_diff_config.sql index ce274a9e0d..04c7dbb484 100644 --- a/server/odc-migrate/src/main/resources/migrate/common/R_2_0_0__initialize_version_diff_config.sql +++ b/server/odc-migrate/src/main/resources/migrate/common/R_2_0_0__initialize_version_diff_config.sql @@ -269,3 +269,10 @@ insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min -- supports ob materialized view insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min_version`,`gmt_create`) values('support_materialized_view', 'OB_MYSQL', 'true', '4.3.5.2', CURRENT_TIMESTAMP) ON DUPLICATE KEY update `config_key`=`config_key`; insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min_version`,`gmt_create`) values('support_materialized_view', 'OB_ORACLE', 'true', '4.3.5.2', CURRENT_TIMESTAMP) ON DUPLICATE KEY update `config_key`=`config_key`; + +-- support sql server datasource +insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min_version`,`gmt_create`) values('column_data_type', 'SQL_SERVER', +'bit:NUMERIC, tinyint:NUMERIC, smallint:NUMERIC, int:NUMERIC, bigint:NUMERIC, decimal:NUMERIC, numeric:NUMERIC, float:NUMERIC, real:NUMERIC, money:NUMERIC, smallmoney:NUMERIC, char:TEXT, varchar:TEXT, nchar:TEXT, nvarchar:TEXT, text:OBJECT, ntext:OBJECT, binary:TEXT, varbinary:TEXT, image:OBJECT, date:DATE, time:TIME, datetime:DATETIME, datetime2:DATETIME, smalldatetime:DATETIME, datetimeoffset:TIMESTAMP, timestamp:OBJECT, uniqueidentifier:OBJECT, xml:OBJECT, sql_variant:OBJECT, hierarchyid:OBJECT, geography:OBJECT, geometry:OBJECT', '0', CURRENT_TIMESTAMP) ON DUPLICATE KEY update `config_key`=`config_key`; +insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min_version`,`gmt_create`) values('support_view','SQL_SERVER','true','0',CURRENT_TIMESTAMP) ON DUPLICATE KEY update `config_key`=`config_key`; +insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min_version`,`gmt_create`) values('support_procedure','SQL_SERVER','true','0',CURRENT_TIMESTAMP) ON DUPLICATE KEY update `config_key`=`config_key`; +insert into `odc_version_diff_config`(`config_key`,`db_mode`,`config_value`,`min_version`,`gmt_create`) values('support_function','SQL_SERVER','true','0',CURRENT_TIMESTAMP) ON DUPLICATE KEY update `config_key`=`config_key`; \ No newline at end of file diff --git a/server/odc-service/pom.xml b/server/odc-service/pom.xml index 5d28d6077c..11c551c199 100644 --- a/server/odc-service/pom.xml +++ b/server/odc-service/pom.xml @@ -296,6 +296,10 @@ org.postgresql postgresql + + com.microsoft.sqlserver + mssql-jdbc + com.oracle.database.nls orai18n diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/ConnectionTesting.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/ConnectionTesting.java index bacc538e3f..e415413e37 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/ConnectionTesting.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/ConnectionTesting.java @@ -157,6 +157,8 @@ public ConnectionTestResult test(@NonNull ConnectionConfig config) { schema = OBConsoleDataSourceFactory.getDefaultSchema(config); } else if (type.getDialectType().isPostgreSql()) { schema = OBConsoleDataSourceFactory.getDefaultSchema(config); + } else if (type.getDialectType().isSqlServer()) { + schema = OBConsoleDataSourceFactory.getDefaultSchema(config); } else { throw new UnsupportedOperationException("Unsupported type, " + type); } diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/model/ConnectionConfig.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/model/ConnectionConfig.java index 24bc1667fb..b207eaa3d2 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/model/ConnectionConfig.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/model/ConnectionConfig.java @@ -384,6 +384,8 @@ public String getDefaultSchema() { return OdcConstants.MYSQL_DEFAULT_SCHEMA; case POSTGRESQL: return OdcConstants.POSTGRESQL_DEFAULT_SCHEMA; + case SQL_SERVER: + return OdcConstants.SQL_SERVER_DEFAULT_SCHEMA; default: return null; } diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/util/ConnectTypeUtil.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/util/ConnectTypeUtil.java index 04cb409545..6764ec81fe 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/util/ConnectTypeUtil.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/connection/util/ConnectTypeUtil.java @@ -101,6 +101,8 @@ private static ConnectType getConnectType(Statement statement, String jdbcUrl) t return ConnectType.MYSQL; case ORACLE: return ConnectType.ORACLE; + case SQL_SERVER: + return ConnectType.SQL_SERVER; } return null; } diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/plugin/ConnectPluginFinder.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/plugin/ConnectPluginFinder.java index 073b01490f..340d92271c 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/plugin/ConnectPluginFinder.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/plugin/ConnectPluginFinder.java @@ -24,6 +24,9 @@ import com.oceanbase.odc.core.shared.constant.DialectType; import com.oceanbase.odc.plugin.connect.api.BaseConnectionPlugin; +import lombok.extern.slf4j.Slf4j; + +@Slf4j class ConnectPluginFinder implements PluginFinder { private volatile Map dialect2PluginId; @@ -44,6 +47,7 @@ protected void init() { if (dialect2PluginId.isEmpty()) { throw new IllegalStateException("BaseConnectionPlugin is empty."); } + log.info("Loaded connection plugins: {}", dialect2PluginId); } @Override @@ -55,7 +59,13 @@ public String findPluginIdBy(DialectType dialectType) { if (pluginId != null) { return pluginId; } - throw new UnsupportedOperationException("Dialect type " + dialectType + " is not supported yet."); + // 提供更详细的错误信息,包括已加载的插件列表 + String loadedDialects = dialect2PluginId.keySet().stream() + .map(DialectType::name) + .collect(java.util.stream.Collectors.joining(", ")); + throw new UnsupportedOperationException( + "Dialect type " + dialectType + " is not supported yet. " + + "Loaded dialects: [" + loadedDialects + "]"); } } diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/session/ConnectConsoleService.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/session/ConnectConsoleService.java index 4a4335d95c..726731462c 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/session/ConnectConsoleService.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/session/ConnectConsoleService.java @@ -110,6 +110,7 @@ import com.oceanbase.tools.dbbrowser.util.MySQLSqlBuilder; import com.oceanbase.tools.dbbrowser.util.OracleSqlBuilder; import com.oceanbase.tools.dbbrowser.util.SqlBuilder; +import com.oceanbase.tools.dbbrowser.util.SqlServerSqlBuilder; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -164,17 +165,41 @@ public SqlExecuteResult queryTableOrViewData(@NotNull String sessionId, sqlBuilder = new OracleSqlBuilder(); } else if (dialectType.isDoris()) { sqlBuilder = new MySQLSqlBuilder(); + } else if (dialectType.isSqlServer()) { + sqlBuilder = new SqlServerSqlBuilder(); } else { throw new IllegalArgumentException("Unsupported dialect type, " + dialectType); } + Integer queryLimit = checkQueryLimit(req.getQueryLimit()); sqlBuilder.append("SELECT "); + if (DialectType.SQL_SERVER == connectionSession.getDialectType()) { + // SQL Server uses TOP clause + sqlBuilder.append("TOP ").append(queryLimit.toString()).append(" "); + } if (req.isAddROWID() && connectionSession.getDialectType().isOracle()) { sqlBuilder.append(" t.ROWID, "); } - sqlBuilder.append(" t.* ").append(" FROM ") - .schemaPrefixIfNotBlank(req.getSchemaName()).identifier(req.getTableOrViewName()).append(" t"); + // For SQL Server, req.getSchemaName() is actually the database name, and 'dbo' is the default schema + String schemaName = req.getSchemaName(); + String databaseName = null; + if (DialectType.SQL_SERVER == connectionSession.getDialectType()) { + // In SQL Server, req.getSchemaName() represents the database name + databaseName = req.getSchemaName(); + schemaName = "dbo"; // Default schema for each database + } + + sqlBuilder.append(" t.* ").append(" FROM "); + + // For SQL Server, use three-part naming: database.schema.table + if (DialectType.SQL_SERVER == connectionSession.getDialectType() && StringUtils.isNotBlank(databaseName)) { + sqlBuilder.identifier(databaseName).append(".") + .identifier(schemaName).append(".") + .identifier(req.getTableOrViewName()).append(" t"); + } else { + // For other databases, use schema.table format + sqlBuilder.schemaPrefixIfNotBlank(schemaName).identifier(req.getTableOrViewName()).append(" t"); + } - Integer queryLimit = checkQueryLimit(req.getQueryLimit()); if (DialectType.OB_ORACLE == connectionSession.getDialectType()) { String version = ConnectionSessionUtil.getVersion(connectionSession); if (VersionUtils.isGreaterThanOrEqualsTo(version, "2.2.50")) { @@ -184,7 +209,8 @@ public SqlExecuteResult queryTableOrViewData(@NotNull String sessionId, } } else if (DialectType.ORACLE == connectionSession.getDialectType()) { sqlBuilder.append(" WHERE ROWNUM <= ").append(queryLimit.toString()); - } else { + } else if (DialectType.SQL_SERVER != connectionSession.getDialectType()) { + // SQL Server already uses TOP clause, skip LIMIT sqlBuilder.append(" LIMIT ").append(queryLimit.toString()); } diff --git a/server/odc-service/src/main/java/com/oceanbase/odc/service/session/factory/OBConsoleDataSourceFactory.java b/server/odc-service/src/main/java/com/oceanbase/odc/service/session/factory/OBConsoleDataSourceFactory.java index cbd1b0b6cf..2f932b28b2 100644 --- a/server/odc-service/src/main/java/com/oceanbase/odc/service/session/factory/OBConsoleDataSourceFactory.java +++ b/server/odc-service/src/main/java/com/oceanbase/odc/service/session/factory/OBConsoleDataSourceFactory.java @@ -314,6 +314,7 @@ public static String getSchema(@NonNull String schema, @NonNull DialectType dial case DORIS: case ODP_SHARDING_OB_MYSQL: case POSTGRESQL: + case SQL_SERVER: return schema; default: return null; @@ -342,6 +343,11 @@ public static String getDefaultSchema(@NonNull ConnectionConfig connectionConfig return getSchema(defaultSchema, connectionConfig.getDialectType()); } return getSchema(OdcConstants.POSTGRESQL_DEFAULT_SCHEMA, connectionConfig.getDialectType()); + case SQL_SERVER: + if (StringUtils.isNotEmpty(defaultSchema)) { + return getSchema(defaultSchema, connectionConfig.getDialectType()); + } + return getSchema(defaultSchema, connectionConfig.getDialectType()); default: return null; } diff --git a/server/plugins/connect-plugin-sqlserver/pom.xml b/server/plugins/connect-plugin-sqlserver/pom.xml new file mode 100644 index 0000000000..f4f66b98e4 --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + plugin-parent + com.oceanbase + 4.3.4-SNAPSHOT + ../pom.xml + + connect-plugin-sqlserver + + + ${project.parent.parent.basedir} + com.oceanbase.odc.plugin.connect.sqlserver.SqlServerConnectionPlugin + connect-plugin-ob-mysql + + + + + com.oceanbase + connect-plugin-api + provided + + + com.oceanbase + connect-plugin-ob-mysql + ${project.version} + + + com.microsoft.sqlserver + mssql-jdbc + provided + + + junit + junit + + + com.oceanbase + odc-test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + diff --git a/server/plugins/connect-plugin-sqlserver/src/main/assembly/assembly.xml b/server/plugins/connect-plugin-sqlserver/src/main/assembly/assembly.xml new file mode 100644 index 0000000000..16b9ba39ae --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/src/main/assembly/assembly.xml @@ -0,0 +1,15 @@ + + + ${project.version} + false + + jar + + + + ${project.build.directory}/classes + / + + + diff --git a/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerConnectionExtension.java b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerConnectionExtension.java new file mode 100644 index 0000000000..3a9a4f147f --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerConnectionExtension.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.connect.sqlserver; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.Validate; +import org.pf4j.Extension; + +import com.oceanbase.odc.common.util.ExceptionUtils; +import com.oceanbase.odc.common.util.StringUtils; +import com.oceanbase.odc.core.datasource.ConnectionInitializer; +import com.oceanbase.odc.core.shared.constant.OdcConstants; +import com.oceanbase.odc.plugin.connect.api.JdbcUrlParser; +import com.oceanbase.odc.plugin.connect.api.TestResult; +import com.oceanbase.odc.plugin.connect.model.JdbcUrlProperty; +import com.oceanbase.odc.plugin.connect.obmysql.OBMySQLConnectionExtension; + +import lombok.NonNull; + +@Extension +public class SqlServerConnectionExtension extends OBMySQLConnectionExtension { + + @Override + public String generateJdbcUrl(@NonNull JdbcUrlProperty properties) { + String host = properties.getHost(); + Validate.notEmpty(host, "host can not be null"); + Integer port = properties.getPort(); + Validate.notNull(port, "port can not be null"); + String catalogName = properties.getCatalogName(); + String schema = properties.getDefaultSchema(); + StringBuilder jdbcUrl = new StringBuilder(); + jdbcUrl.append("jdbc:sqlserver://").append(host).append(":").append(port); + if (StringUtils.isNotBlank(catalogName)) { + jdbcUrl.append(";databaseName=").append(catalogName); + } + if (StringUtils.isNotBlank(schema)) { + jdbcUrl.append(";currentSchema=").append(schema); + } + + // 添加默认的 JDBC 参数 + String parameters = getJdbcUrlParameters(properties.getJdbcParameters()); + if (StringUtils.isNotBlank(parameters)) { + jdbcUrl.append(";").append(parameters.replace("&", ";")); + } + + return jdbcUrl.toString(); + } + + @Override + protected Map appendDefaultJdbcUrlParameters(Map jdbcUrlParams) { + if (jdbcUrlParams == null) { + jdbcUrlParams = new java.util.HashMap<>(); + } + // 设置 trustServerCertificate=true 以跳过 SSL 证书验证 + // 注意:生产环境应该配置正确的 SSL 证书,而不是跳过验证 + if (!jdbcUrlParams.containsKey("trustServerCertificate")) { + jdbcUrlParams.put("trustServerCertificate", "true"); + } + // 设置 encrypt=true 启用加密(即使信任服务器证书) + if (!jdbcUrlParams.containsKey("encrypt")) { + jdbcUrlParams.put("encrypt", "true"); + } + return jdbcUrlParams; + } + + @Override + public TestResult test(String jdbcUrl, Properties properties, + int queryTimeout, List initializers) { + try (Connection connection = DriverManager.getConnection(jdbcUrl, properties)) { + try (Statement statement = connection.createStatement()) { + if (queryTimeout >= 0) { + statement.setQueryTimeout(queryTimeout); + } + if (CollectionUtils.isNotEmpty(initializers)) { + try { + for (ConnectionInitializer initializer : initializers) { + initializer.init(connection); + } + } catch (Exception e) { + return TestResult.initScriptFailed(e); + } + } + return TestResult.success(); + } + } catch (Exception e) { + Throwable rootCause = ExceptionUtils.getRootCause(e); + return TestResult.unknownError(rootCause); + } + } + + private TestResult executeTest(Connection connection, int queryTimeout, + List initializers) throws SQLException { + try (Statement statement = connection.createStatement()) { + if (queryTimeout >= 0) { + statement.setQueryTimeout(queryTimeout); + } + if (CollectionUtils.isNotEmpty(initializers)) { + try { + for (ConnectionInitializer initializer : initializers) { + initializer.init(connection); + } + } catch (Exception e) { + return TestResult.initScriptFailed(e); + } + } + return TestResult.success(); + } + } + + @Override + public String getDriverClassName() { + return OdcConstants.SQL_SERVER_DRIVER_CLASS_NAME; + } + + @Override + public List getConnectionInitializers() { + return Collections.emptyList(); + } + + @Override + public JdbcUrlParser getConnectionInfo(@NonNull String jdbcUrl, String userName) throws SQLException { + return new SqlServerJdbcUrlParser(jdbcUrl, userName); + } + +} diff --git a/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerConnectionPlugin.java b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerConnectionPlugin.java new file mode 100644 index 0000000000..150c584281 --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerConnectionPlugin.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.connect.sqlserver; + +import com.oceanbase.odc.core.shared.constant.DialectType; +import com.oceanbase.odc.plugin.connect.api.BaseConnectionPlugin; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +public class SqlServerConnectionPlugin extends BaseConnectionPlugin { + @Override + public DialectType getDialectType() { + return DialectType.SQL_SERVER; + } +} diff --git a/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerInformationExtension.java b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerInformationExtension.java new file mode 100644 index 0000000000..caa83a7053 --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerInformationExtension.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.connect.sqlserver; + +import java.sql.Connection; + +import org.pf4j.Extension; + +import com.oceanbase.odc.common.util.JdbcOperationsUtil; +import com.oceanbase.odc.plugin.connect.api.InformationExtensionPoint; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Slf4j +@Extension +public class SqlServerInformationExtension implements InformationExtensionPoint { + + @Override + public String getDBVersion(Connection connection) { + String querySql = "SELECT SERVERPROPERTY('ProductVersion')"; + try { + String version = JdbcOperationsUtil.getJdbcOperations(connection) + .queryForObject(querySql, String.class); + if (version != null) { + return version; + } + // Fallback to @@VERSION if SERVERPROPERTY returns null + querySql = "SELECT @@VERSION"; + return JdbcOperationsUtil.getJdbcOperations(connection) + .queryForObject(querySql, String.class); + } catch (Exception e) { + log.warn("Failed to get SQL Server version, will return a default version", e); + return "14.0.0"; + } + } +} diff --git a/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerJdbcUrlParser.java b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerJdbcUrlParser.java new file mode 100644 index 0000000000..ab01d4e578 --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerJdbcUrlParser.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.connect.sqlserver; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.oceanbase.odc.plugin.connect.api.HostAddress; +import com.oceanbase.odc.plugin.connect.api.JdbcUrlParser; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Slf4j +public class SqlServerJdbcUrlParser implements JdbcUrlParser { + private static final String SQL_SERVER_JDBC_PREFIX = "jdbc:sqlserver://"; + private static final Pattern URL_PATTERN = Pattern.compile( + "jdbc:sqlserver://([^:;]+):(\\d+)(?:;([^;]*))?"); + private static final Pattern DATABASE_NAME_PATTERN = Pattern.compile("databaseName=([^;]+)"); + private static final Pattern SCHEMA_PATTERN = Pattern.compile("currentSchema=([^;]+)"); + + private final String jdbcUrl; + private final List addresses; + private final Map parameters; + private final String schema; + + public SqlServerJdbcUrlParser(@NonNull String jdbcUrl, String userName) throws SQLException { + if (!jdbcUrl.startsWith(SQL_SERVER_JDBC_PREFIX)) { + throw new IllegalArgumentException("Invalid JDBC URL for SQL Server: " + jdbcUrl); + } + this.jdbcUrl = jdbcUrl; + this.addresses = parseHostAndPort(jdbcUrl); + this.parameters = parseParameters(jdbcUrl); + this.schema = parseSchema(jdbcUrl); + } + + private List parseHostAndPort(String jdbcUrl) throws SQLException { + Matcher matcher = URL_PATTERN.matcher(jdbcUrl); + if (matcher.find()) { + HostAddress hostAddress = new HostAddress(); + hostAddress.setHost(matcher.group(1)); + hostAddress.setPort(Integer.parseInt(matcher.group(2))); + return Collections.singletonList(hostAddress); + } + throw new SQLException("Failed to parse host and port from JDBC URL: " + jdbcUrl); + } + + private Map parseParameters(String jdbcUrl) { + Map paramsMap = new HashMap<>(); + + int paramsIndex = jdbcUrl.indexOf(';'); + if (paramsIndex > 0) { + String paramsString = jdbcUrl.substring(paramsIndex + 1); + String[] paramsArray = paramsString.split(";"); + for (String param : paramsArray) { + int equalIndex = param.indexOf('='); + if (equalIndex > 0) { + String key = param.substring(0, equalIndex); + String value = param.substring(equalIndex + 1); + // Skip databaseName and currentSchema as they are handled separately + if (!"databaseName".equals(key) && !"currentSchema".equals(key)) { + paramsMap.put(key, value); + } + } + } + } + return paramsMap; + } + + private String parseSchema(String jdbcUrl) { + Matcher schemaMatcher = SCHEMA_PATTERN.matcher(jdbcUrl); + if (schemaMatcher.find()) { + return schemaMatcher.group(1); + } + // Default schema for SQL Server is 'dbo' + return "dbo"; + } + + @Override + public List getHostAddresses() { + return this.addresses; + } + + @Override + public String getSchema() { + return this.schema; + } + + @Override + public Map getParameters() { + return this.parameters; + } +} diff --git a/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerSessionExtension.java b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerSessionExtension.java new file mode 100644 index 0000000000..9ed3b023fd --- /dev/null +++ b/server/plugins/connect-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/connect/sqlserver/SqlServerSessionExtension.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.connect.sqlserver; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.pf4j.Extension; + +import com.oceanbase.odc.common.util.JdbcOperationsUtil; +import com.oceanbase.odc.plugin.connect.model.DBClientInfo; +import com.oceanbase.odc.plugin.connect.obmysql.OBMySQLSessionExtension; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Slf4j +@Extension +public class SqlServerSessionExtension extends OBMySQLSessionExtension { + + @Override + public void switchSchema(Connection connection, String schemaName) throws SQLException { + String currentSchema = getCurrentSchema(connection); + if (currentSchema != null && currentSchema.equals(schemaName)) { + return; + } + // SQL Server uses USE statement or SET SCHEMA + String sql = "USE " + schemaName; + JdbcOperationsUtil.getJdbcOperations(connection).execute(sql); + } + + @Override + public String getCurrentSchema(Connection connection) { + String querySql = "SELECT SCHEMA_NAME()"; + try { + return JdbcOperationsUtil.getJdbcOperations(connection) + .queryForObject(querySql, String.class); + } catch (Exception e) { + // Fallback to default schema + return "dbo"; + } + } + + @Override + public String getConnectionId(Connection connection) { + // SQL Server uses @@SPID to get the session process ID + String querySql = "SELECT @@SPID"; + try { + return JdbcOperationsUtil.getJdbcOperations(connection).queryForObject(querySql, String.class); + } catch (Exception e) { + log.warn("Failed to get connection ID from SQL Server using @@SPID", e); + // Fallback: try to get connection ID from sys.dm_exec_sessions + try { + querySql = "SELECT session_id FROM sys.dm_exec_sessions WHERE session_id = @@SPID"; + return JdbcOperationsUtil.getJdbcOperations(connection).queryForObject(querySql, String.class); + } catch (Exception ex) { + log.warn("Failed to get connection ID from sys.dm_exec_sessions", ex); + // Return empty string as fallback + return ""; + } + } + } + + @Override + public String getKillQuerySql(@NonNull String connectionId) { + // SQL Server doesn't have a separate KILL QUERY command + // Use KILL to terminate the session (which will also kill the query) + return getKillSessionSql(connectionId); + } + + @Override + public String getKillSessionSql(@NonNull String connectionId) { + // SQL Server uses KILL to terminate a session + return "KILL " + connectionId; + } + + @Override + public boolean setClientInfo(Connection connection, DBClientInfo clientInfo) { + // SQL Server doesn't support setting client info in the same way as MySQL + return false; + } +} diff --git a/server/plugins/pom.xml b/server/plugins/pom.xml index e147a8b51b..1e2c6ea5bd 100644 --- a/server/plugins/pom.xml +++ b/server/plugins/pom.xml @@ -20,6 +20,7 @@ connect-plugin-doris connect-plugin-oracle connect-plugin-postgres + connect-plugin-sqlserver schema-plugin-api schema-plugin-ob-mysql schema-plugin-ob-oracle @@ -27,6 +28,7 @@ schema-plugin-doris schema-plugin-oracle schema-plugin-postgres + schema-plugin-sqlserver schema-plugin-odp-sharding-ob-mysql task-plugin-api task-plugin-mysql @@ -95,6 +97,12 @@ ${project.version} provided + + com.oceanbase + connect-plugin-sqlserver + ${project.version} + provided + com.oceanbase task-plugin-ob-mysql @@ -149,6 +157,12 @@ ${project.version} provided + + com.oceanbase + schema-plugin-sqlserver + ${project.version} + provided + diff --git a/server/plugins/schema-plugin-sqlserver/pom.xml b/server/plugins/schema-plugin-sqlserver/pom.xml new file mode 100644 index 0000000000..834c5aac28 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/pom.xml @@ -0,0 +1,64 @@ + + + + + 4.0.0 + + plugin-parent + com.oceanbase + 4.3.4-SNAPSHOT + ../pom.xml + + + + schema-plugin-sqlserver + + + ${project.parent.parent.basedir} + com.oceanbase.odc.plugin.schema.sqlserver.SqlServerSchemaPlugin + schema-plugin-ob-mysql,connect-plugin-sqlserver + + + + + com.oceanbase + connect-plugin-sqlserver + ${project.version} + + + com.oceanbase + schema-plugin-api + + + com.oceanbase + schema-plugin-ob-mysql + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + diff --git a/server/plugins/schema-plugin-sqlserver/src/main/assembly/assembly.xml b/server/plugins/schema-plugin-sqlserver/src/main/assembly/assembly.xml new file mode 100644 index 0000000000..16b9ba39ae --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/assembly/assembly.xml @@ -0,0 +1,15 @@ + + + ${project.version} + false + + jar + + + + ${project.build.directory}/classes + / + + + diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerDatabaseExtension.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerDatabaseExtension.java new file mode 100644 index 0000000000..6a47740354 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerDatabaseExtension.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver; + +import java.sql.Connection; + +import org.pf4j.Extension; + +import com.oceanbase.odc.plugin.schema.obmysql.OBMySQLDatabaseExtension; +import com.oceanbase.odc.plugin.schema.sqlserver.utils.DBAccessorUtil; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Extension +public class SqlServerDatabaseExtension extends OBMySQLDatabaseExtension { + + @Override + protected DBSchemaAccessor getSchemaAccessor(Connection connection) { + return DBAccessorUtil.getSchemaAccessor(connection); + } + +} diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerFunctionExtension.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerFunctionExtension.java new file mode 100644 index 0000000000..cb863027b3 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerFunctionExtension.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver; + +import java.sql.Connection; + +import org.pf4j.Extension; + +import com.oceanbase.odc.plugin.schema.obmysql.OBMySQLFunctionExtension; +import com.oceanbase.odc.plugin.schema.sqlserver.utils.DBAccessorUtil; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Extension +public class SqlServerFunctionExtension extends OBMySQLFunctionExtension { + + @Override + protected DBSchemaAccessor getSchemaAccessor(Connection connection) { + return DBAccessorUtil.getSchemaAccessor(connection); + } + +} diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerProcedureExtension.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerProcedureExtension.java new file mode 100644 index 0000000000..29ba3371a1 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerProcedureExtension.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver; + +import java.sql.Connection; + +import org.pf4j.Extension; + +import com.oceanbase.odc.plugin.schema.obmysql.OBMySQLProcedureExtension; +import com.oceanbase.odc.plugin.schema.sqlserver.utils.DBAccessorUtil; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Extension +public class SqlServerProcedureExtension extends OBMySQLProcedureExtension { + + @Override + protected DBSchemaAccessor getSchemaAccessor(Connection connection) { + return DBAccessorUtil.getSchemaAccessor(connection); + } + +} diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerSchemaPlugin.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerSchemaPlugin.java new file mode 100644 index 0000000000..79d08ac79f --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerSchemaPlugin.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver; + +import com.oceanbase.odc.core.shared.constant.DialectType; +import com.oceanbase.odc.plugin.schema.api.BaseSchemaPlugin; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +public class SqlServerSchemaPlugin extends BaseSchemaPlugin { + @Override + public DialectType getDialectType() { + return DialectType.SQL_SERVER; + } +} diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerTableExtension.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerTableExtension.java new file mode 100644 index 0000000000..d2edb33e14 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerTableExtension.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver; + +import java.sql.Connection; + +import org.pf4j.Extension; + +import com.oceanbase.odc.common.unit.BinarySizeUnit; +import com.oceanbase.odc.plugin.schema.obmysql.OBMySQLTableExtension; +import com.oceanbase.odc.plugin.schema.sqlserver.utils.DBAccessorUtil; +import com.oceanbase.tools.dbbrowser.editor.DBTableEditor; +import com.oceanbase.tools.dbbrowser.model.DBObjectType; +import com.oceanbase.tools.dbbrowser.model.DBTable; +import com.oceanbase.tools.dbbrowser.model.DBTableStats; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; +import com.oceanbase.tools.dbbrowser.stats.DBStatsAccessor; + +import lombok.NonNull; + +@Extension +public class SqlServerTableExtension extends OBMySQLTableExtension { + + @Override + public DBTable getDetail(@NonNull Connection connection, @NonNull String schemaName, @NonNull String tableName) { + DBSchemaAccessor schemaAccessor = getSchemaAccessor(connection); + DBTable table = new DBTable(); + table.setSchemaName(schemaName); + table.setOwner(schemaName); + table.setName(tableName); + table.setColumns(schemaAccessor.listTableColumns(schemaName, tableName)); + table.setPartition(schemaAccessor.getPartition(schemaName, tableName)); + if (!schemaAccessor.isExternalTable(schemaName, tableName)) { + table.setConstraints(schemaAccessor.listTableConstraints(schemaName, tableName)); + table.setIndexes(schemaAccessor.listTableIndexes(schemaName, tableName)); + table.setType(DBObjectType.TABLE); + } else { + table.setType(DBObjectType.EXTERNAL_TABLE); + } + table.setDDL(schemaAccessor.getTableDDL(schemaName, tableName)); + table.setTableOptions(schemaAccessor.getTableOptions(schemaName, tableName)); + table.setStats(getTableStats(connection, schemaName, tableName)); + return table; + } + + @Override + protected DBTableEditor getTableEditor(@NonNull Connection connection) { + return DBAccessorUtil.getTableEditor(connection); + } + + @Override + protected DBSchemaAccessor getSchemaAccessor(Connection connection) { + return DBAccessorUtil.getSchemaAccessor(connection); + } + + @Override + protected DBStatsAccessor getStatsAccessor(Connection connection) { + return DBAccessorUtil.getStatsAccessor(connection); + } + + @Override + protected DBTableStats getTableStats(@NonNull Connection connection, @NonNull String schemaName, + @NonNull String tableName) { + DBStatsAccessor statsAccessor = getStatsAccessor(connection); + // SQL Server 的 statsAccessor 目前返回 null,需要处理这种情况 + if (statsAccessor == null) { + return new DBTableStats(); + } + DBTableStats tableStats = statsAccessor.getTableStats(schemaName, tableName); + if (tableStats == null) { + return new DBTableStats(); + } + Long dataSizeInBytes = tableStats.getDataSizeInBytes(); + if (dataSizeInBytes == null || dataSizeInBytes < 0) { + tableStats.setTableSize(null); + } else { + tableStats.setTableSize(BinarySizeUnit.B.of(dataSizeInBytes).toString()); + } + return tableStats; + } + + @Override + public boolean syncExternalTableFiles(Connection connection, String schemaName, String tableName) { + throw new UnsupportedOperationException("not implemented yet"); + } +} diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerViewExtension.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerViewExtension.java new file mode 100644 index 0000000000..7e304eef77 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/SqlServerViewExtension.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver; + +import java.sql.Connection; + +import org.pf4j.Extension; + +import com.oceanbase.odc.plugin.schema.obmysql.OBMySQLViewExtension; +import com.oceanbase.odc.plugin.schema.sqlserver.utils.DBAccessorUtil; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +@Extension +public class SqlServerViewExtension extends OBMySQLViewExtension { + + @Override + protected DBSchemaAccessor getSchemaAccessor(Connection connection) { + return DBAccessorUtil.getSchemaAccessor(connection); + } + +} diff --git a/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/utils/DBAccessorUtil.java b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/utils/DBAccessorUtil.java new file mode 100644 index 0000000000..539315dde7 --- /dev/null +++ b/server/plugins/schema-plugin-sqlserver/src/main/java/com/oceanbase/odc/plugin/schema/sqlserver/utils/DBAccessorUtil.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.oceanbase.odc.plugin.schema.sqlserver.utils; + +import java.sql.Connection; + +import com.oceanbase.odc.common.util.JdbcOperationsUtil; +import com.oceanbase.odc.core.shared.constant.DialectType; +import com.oceanbase.odc.plugin.connect.sqlserver.SqlServerInformationExtension; +import com.oceanbase.tools.dbbrowser.DBBrowser; +import com.oceanbase.tools.dbbrowser.editor.DBTableEditor; +import com.oceanbase.tools.dbbrowser.schema.DBSchemaAccessor; +import com.oceanbase.tools.dbbrowser.stats.DBStatsAccessor; + +/** + * @author yizhou.xw + * @date 2024/12 + * @since ODC_release_4.3.4 + */ +public class DBAccessorUtil { + + public static String getDbVersion(Connection connection) { + return new SqlServerInformationExtension().getDBVersion(connection); + } + + public static DBSchemaAccessor getSchemaAccessor(Connection connection) { + return DBBrowser.schemaAccessor() + .setJdbcOperations(JdbcOperationsUtil.getJdbcOperations(connection)) + .setType(DialectType.SQL_SERVER.getDBBrowserDialectTypeName()).create(); + } + + public static DBStatsAccessor getStatsAccessor(Connection connection) { + return DBBrowser.statsAccessor() + .setJdbcOperations(JdbcOperationsUtil.getJdbcOperations(connection)) + .setDbVersion(getDbVersion(connection)) + .setType(DialectType.SQL_SERVER.getDBBrowserDialectTypeName()).create(); + } + + public static DBTableEditor getTableEditor(Connection connection) { + return DBBrowser.objectEditor().tableEditor() + .setDbVersion(getDbVersion(connection)) + .setType(DialectType.SQL_SERVER.getDBBrowserDialectTypeName()).create(); + } + +}