diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ConnectionManager.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ConnectionManager.java index 6ac3d2f..3d0d94b 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ConnectionManager.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ConnectionManager.java @@ -20,6 +20,8 @@ public interface ConnectionManager { void setDatabaseContext(String database); + T withDatabaseContext(String database, DatabaseContextAction action) throws SQLException; + Datasource getDatasource(); DBConnectInfo getConnectInfo(); @@ -37,4 +39,9 @@ public interface ConnectionManager { String getDatabaseContextKey(); void close(); + + @FunctionalInterface + interface DatabaseContextAction { + T run() throws SQLException; + } } diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ResourceBrowser.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ResourceBrowser.java index de4eff9..5e1b381 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ResourceBrowser.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ResourceBrowser.java @@ -17,6 +17,14 @@ public interface ResourceBrowser { List getChildren(TreeNode node, boolean fromCache) throws SQLException; + List getSchemas() throws SQLException; + + List getTables(String schema) throws SQLException; + + List getViews(String schema) throws SQLException; + + List getFields(String schema, String table) throws SQLException; + List getSchemas(SQL sql) throws SQLException; List
getTables(SQL sql) throws SQLException; diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java index f9c0b22..96c80f4 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java @@ -76,7 +76,26 @@ public Driver getDriver() { @Override public void setDatabaseContext(String database) { - this.currentDatabase.set(database); + if (StringUtils.isBlank(database)) { + this.currentDatabase.remove(); + } else { + this.currentDatabase.set(database); + } + } + + @Override + public T withDatabaseContext(String database, DatabaseContextAction action) throws SQLException { + var previousDatabase = this.currentDatabase.get(); + try { + this.setDatabaseContext(database); + return action.run(); + } finally { + if (previousDatabase == null) { + this.currentDatabase.remove(); + } else { + this.currentDatabase.set(previousDatabase); + } + } } @Override diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/QualifiedRelation.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/QualifiedRelation.java new file mode 100644 index 0000000..7c8254c --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/QualifiedRelation.java @@ -0,0 +1,9 @@ +package org.jumpserver.chen.framework.datasource.metadata; + +public record QualifiedRelation( + String catalog, + String schema, + String name, + String kind +) { +} diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/RelationColumnsMetadata.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/RelationColumnsMetadata.java new file mode 100644 index 0000000..d2d9eef --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/RelationColumnsMetadata.java @@ -0,0 +1,9 @@ +package org.jumpserver.chen.framework.datasource.metadata; + +import java.util.List; + +public record RelationColumnsMetadata( + QualifiedRelation relation, + List columns +) { +} diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/RelationMetadataPage.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/RelationMetadataPage.java new file mode 100644 index 0000000..e488d17 --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/RelationMetadataPage.java @@ -0,0 +1,9 @@ +package org.jumpserver.chen.framework.datasource.metadata; + +import java.util.List; + +public record RelationMetadataPage( + List items, + boolean truncated +) { +} diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlColumnMetadata.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlColumnMetadata.java new file mode 100644 index 0000000..0c6026e --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlColumnMetadata.java @@ -0,0 +1,8 @@ +package org.jumpserver.chen.framework.datasource.metadata; + +public record SqlColumnMetadata( + String name, + String dataType, + boolean nullable +) { +} diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlMetadataCatalog.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlMetadataCatalog.java new file mode 100644 index 0000000..15101be --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/metadata/SqlMetadataCatalog.java @@ -0,0 +1,275 @@ +package org.jumpserver.chen.framework.datasource.metadata; + +import org.apache.commons.lang3.StringUtils; +import org.jumpserver.chen.framework.datasource.ConnectionManager; +import org.jumpserver.chen.framework.datasource.ResourceBrowser; +import org.jumpserver.chen.framework.datasource.entity.resource.ResourceNodeSnapshot; +import org.jumpserver.chen.framework.utils.SqlIdentifierUtils; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class SqlMetadataCatalog { + public static final int DEFAULT_RELATION_LIMIT = 100; + public static final int MAX_RELATION_LIMIT = 200; + public static final int MAX_COLUMN_RELATIONS = 20; + + private static final Set RELATION_KINDS = Set.of("table", "view"); + + private final ResourceBrowser resourceBrowser; + private final ConnectionManager connectionManager; + + public SqlMetadataCatalog(ResourceBrowser resourceBrowser, ConnectionManager connectionManager) { + this.resourceBrowser = Objects.requireNonNull(resourceBrowser); + this.connectionManager = Objects.requireNonNull(connectionManager); + } + + public RelationMetadataPage listRelations(ResourceNodeSnapshot node, String context, String prefix, Integer limit) + throws SQLException { + var scope = connectionManager.withDatabaseContext(null, () -> this.resolveScope(node, context)); + return connectionManager.withDatabaseContext( + scope.catalog(), + () -> this.listRelations(scope, prefix, limit) + ); + } + + private RelationMetadataPage listRelations(MetadataScope scope, String prefix, Integer limit) throws SQLException { + var schemas = this.resolveSchemas(scope.schema()); + var relations = new ArrayList(); + + for (var schema : schemas) { + resourceBrowser.getTables(schema).forEach(table -> relations.add( + new QualifiedRelation(scope.catalog(), schema, table.getName(), "table") + )); + resourceBrowser.getViews(schema).forEach(view -> relations.add( + new QualifiedRelation(scope.catalog(), schema, view.getName(), "view") + )); + } + + var normalizedPrefix = StringUtils.defaultString(prefix).trim().toLowerCase(Locale.ROOT); + var filtered = relations.stream() + .filter(relation -> normalizedPrefix.isEmpty() + || relation.name().toLowerCase(Locale.ROOT).startsWith(normalizedPrefix) + || (relation.schema() + "." + relation.name()).toLowerCase(Locale.ROOT) + .startsWith(normalizedPrefix)) + .sorted(Comparator.comparing(QualifiedRelation::schema, String.CASE_INSENSITIVE_ORDER) + .thenComparing(QualifiedRelation::name, String.CASE_INSENSITIVE_ORDER) + .thenComparing(QualifiedRelation::kind)) + .toList(); + + var boundedLimit = limit == null + ? DEFAULT_RELATION_LIMIT + : Math.max(1, Math.min(limit, MAX_RELATION_LIMIT)); + return new RelationMetadataPage( + List.copyOf(filtered.subList(0, Math.min(filtered.size(), boundedLimit))), + filtered.size() > boundedLimit + ); + } + + public List listColumns( + ResourceNodeSnapshot node, + String context, + List requestedRelations + ) throws SQLException { + if (requestedRelations == null || requestedRelations.isEmpty()) { + return List.of(); + } + if (requestedRelations.size() > MAX_COLUMN_RELATIONS) { + throw new IllegalArgumentException("Too many relations in one metadata request"); + } + + var scope = connectionManager.withDatabaseContext(null, () -> this.resolveScope(node, context)); + return connectionManager.withDatabaseContext( + scope.catalog(), + () -> this.listColumns(scope, requestedRelations) + ); + } + + private List listColumns( + MetadataScope scope, + List requestedRelations + ) throws SQLException { + var availableSchemas = this.resolveSchemas(null); + var relationsBySchema = new LinkedHashMap>(); + var canonicalRequests = new LinkedHashMap(); + + for (var requested : requestedRelations) { + this.validateRequestedRelation(requested, scope.catalog()); + var requestedSchema = StringUtils.defaultIfBlank(requested.schema(), scope.schema()); + var canonicalSchema = this.resolveCanonicalIdentifier( + availableSchemas, + requestedSchema, + "Unknown relation schema" + ); + + var availableRelations = relationsBySchema.get(canonicalSchema); + if (availableRelations == null) { + availableRelations = this.loadRelationsByKey(scope.catalog(), canonicalSchema); + relationsBySchema.put(canonicalSchema, availableRelations); + } + var key = new RelationKey(canonicalSchema, requested.name(), requested.kind()); + var canonical = availableRelations.get(key); + if (canonical == null) { + var canonicalName = this.resolveCanonicalIdentifier( + availableRelations.values().stream() + .filter(relation -> relation.kind().equals(requested.kind())) + .map(QualifiedRelation::name) + .toList(), + requested.name(), + "Unknown relation" + ); + canonical = availableRelations.get(new RelationKey(canonicalSchema, canonicalName, requested.kind())); + } + var canonicalKey = new RelationKey(canonical.schema(), canonical.name(), canonical.kind()); + canonicalRequests.putIfAbsent(canonicalKey, canonical); + } + + var result = new ArrayList(); + for (var entry : canonicalRequests.entrySet()) { + var relation = entry.getValue(); + var columns = resourceBrowser.getFields(relation.schema(), relation.name()).stream() + .map(field -> new SqlColumnMetadata(field.getName(), field.getType(), field.isNullable())) + .toList(); + result.add(new RelationColumnsMetadata(relation, columns)); + } + return List.copyOf(result); + } + + private MetadataScope resolveScope(ResourceNodeSnapshot node, String context) throws SQLException { + if (node == null) { + throw new IllegalArgumentException("Invalid metadata context"); + } + + var contextKey = connectionManager.getContextKey(); + var databaseContextKey = connectionManager.getDatabaseContextKey(); + var currentContext = StringUtils.defaultString(context).trim(); + var catalog = node.database(); + + if (StringUtils.equals(contextKey, databaseContextKey) && StringUtils.isNotBlank(currentContext)) { + var allowedContexts = connectionManager.getSqlActuator().getSchemas(); + if (!allowedContexts.contains(currentContext)) { + throw new IllegalArgumentException("Unknown database metadata context"); + } + catalog = currentContext; + } + SqlIdentifierUtils.validateDatabaseName(catalog); + + String schema = null; + if (StringUtils.equals(contextKey, "schema")) { + schema = StringUtils.defaultIfBlank(currentContext, node.schema()); + if (StringUtils.isNotBlank(catalog) && schema.startsWith(catalog + ".")) { + schema = schema.substring(catalog.length() + 1); + } + } else if (StringUtils.equals(node.database(), catalog)) { + schema = node.schema(); + } + return new MetadataScope(catalog, schema); + } + + private List resolveSchemas(String requestedSchema) throws SQLException { + var schemas = resourceBrowser.getSchemas().stream().map(schema -> schema.getName()).toList(); + if (StringUtils.isBlank(requestedSchema)) { + return schemas; + } + return List.of(this.resolveCanonicalIdentifier(schemas, requestedSchema, "Unknown metadata schema")); + } + + private String resolveCanonicalIdentifier( + Collection candidates, + String requested, + String unknownMessage + ) { + if (StringUtils.isBlank(requested)) { + throw new IllegalArgumentException(unknownMessage); + } + + var exactMatches = candidates.stream().filter(requested::equals).distinct().toList(); + if (exactMatches.size() == 1) { + return exactMatches.get(0); + } + + var caseRule = this.identifierCaseRule(); + if (caseRule == IdentifierCaseRule.LOWER || caseRule == IdentifierCaseRule.UPPER) { + var normalizedRequested = caseRule.normalize(requested); + var normalizedMatches = candidates.stream() + .filter(candidate -> candidate.equals(caseRule.normalize(candidate))) + .filter(candidate -> candidate.equals(normalizedRequested)) + .distinct() + .toList(); + if (normalizedMatches.size() == 1) { + return normalizedMatches.get(0); + } + throw new IllegalArgumentException(unknownMessage); + } + + if (caseRule == IdentifierCaseRule.INSENSITIVE) { + var insensitiveMatches = candidates.stream() + .filter(candidate -> candidate.equalsIgnoreCase(requested)) + .distinct() + .toList(); + if (insensitiveMatches.size() == 1) { + return insensitiveMatches.get(0); + } + } + throw new IllegalArgumentException(unknownMessage); + } + + private IdentifierCaseRule identifierCaseRule() { + var connectInfo = connectionManager.getConnectInfo(); + var dbType = connectInfo == null ? "" : StringUtils.defaultString(connectInfo.getDbType()); + return switch (dbType.toLowerCase(Locale.ROOT)) { + case "postgresql" -> IdentifierCaseRule.LOWER; + case "oracle", "db2", "dm", "dameng" -> IdentifierCaseRule.UPPER; + case "mysql", "mariadb", "sqlserver" -> IdentifierCaseRule.INSENSITIVE; + default -> IdentifierCaseRule.EXACT; + }; + } + + private Map loadRelationsByKey(String catalog, String schema) throws SQLException { + var result = new LinkedHashMap(); + resourceBrowser.getTables(schema).forEach(table -> { + var relation = new QualifiedRelation(catalog, schema, table.getName(), "table"); + result.put(new RelationKey(schema, relation.name(), relation.kind()), relation); + }); + resourceBrowser.getViews(schema).forEach(view -> { + var relation = new QualifiedRelation(catalog, schema, view.getName(), "view"); + result.put(new RelationKey(schema, relation.name(), relation.kind()), relation); + }); + return result; + } + + private void validateRequestedRelation(QualifiedRelation relation, String catalog) { + if (relation == null || StringUtils.isBlank(relation.name()) || !RELATION_KINDS.contains(relation.kind())) { + throw new IllegalArgumentException("Invalid relation metadata request"); + } + if (StringUtils.isNotBlank(relation.catalog()) && StringUtils.isNotBlank(catalog) + && !relation.catalog().equals(catalog)) { + throw new IllegalArgumentException("Relation catalog does not match the active context"); + } + } + + private record MetadataScope(String catalog, String schema) { + } + + private record RelationKey(String schema, String name, String kind) { + } + + private enum IdentifierCaseRule { + LOWER, + UPPER, + INSENSITIVE, + EXACT; + + private String normalize(String identifier) { + return this == UPPER ? identifier.toUpperCase(Locale.ROOT) : identifier.toLowerCase(Locale.ROOT); + } + } +} diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/clickhouse/ClickhouseResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/clickhouse/ClickhouseResourceBrowser.java index 92427c4..e19086d 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/clickhouse/ClickhouseResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/clickhouse/ClickhouseResourceBrowser.java @@ -38,7 +38,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME, COLUMN_TYPE AS TYPE, COLUMN_KEY AS `KEY`, IS_NULLABLE AS `NULLABLE`, COLUMN_DEFAULT AS `DEFAULT`, EXTRA AS EXTRA, COLUMN_COMMENT AS COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '?' AND TABLE_NAME = '?'"; + private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME, DATA_TYPE AS TYPE, IS_NULLABLE AS NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '?' AND TABLE_NAME = '?'"; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/dameng/DMResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/dameng/DMResourceBrowser.java index 5fa4d5d..3037f19 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/dameng/DMResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/dameng/DMResourceBrowser.java @@ -38,7 +38,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS `NAME`,DATA_TYPE AS `TYPE`,DATA_LENGTH AS `LENGTH`,DATA_DEFAULT AS `DEFAULT` FROM ALL_TAB_COLUMNS WHERE OWNER='?' AND TABLE_NAME='?'"; + private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME,DATA_TYPE AS TYPE,NULLABLE AS NULLABLE FROM ALL_TAB_COLUMNS WHERE OWNER='?' AND TABLE_NAME='?'"; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/db2/DB2ResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/db2/DB2ResourceBrowser.java index 11d497b..61124b3 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/db2/DB2ResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/db2/DB2ResourceBrowser.java @@ -38,7 +38,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "select COLNAME, TYPENAME, LENGTH, DEFAULT, REMARKS from syscat.COLUMNS where TABSCHEMA = '?' and TABNAME = '?'"; + private static final String SQL_GET_FIELDS = "select COLNAME, TYPENAME, NULLS from syscat.COLUMNS where TABSCHEMA = '?' and TABNAME = '?'"; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlResourceBrowser.java index e6803c8..1611b04 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlResourceBrowser.java @@ -38,7 +38,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME, COLUMN_TYPE AS TYPE, COLUMN_KEY AS `KEY`, IS_NULLABLE AS `NULLABLE`, COLUMN_DEFAULT AS `DEFAULT`, EXTRA AS EXTRA, COLUMN_COMMENT AS COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '?' AND TABLE_NAME = '?'"; + private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME, COLUMN_TYPE AS TYPE, IS_NULLABLE AS NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '?' AND TABLE_NAME = '?'"; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/oracle/OracleResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/oracle/OracleResourceBrowser.java index 26c0c47..4034510 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/oracle/OracleResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/oracle/OracleResourceBrowser.java @@ -38,7 +38,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME,DATA_TYPE,DATA_LENGTH,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,CHAR_USED,DATA_DEFAULT,COMMENTS FROM ALL_TAB_COLUMNS WHERE OWNER='?' AND TABLE_NAME='?'" ; + private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME,DATA_TYPE,NULLABLE FROM ALL_TAB_COLUMNS WHERE OWNER='?' AND TABLE_NAME='?'" ; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlResourceBrowser.java index fb51096..e34d875 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlResourceBrowser.java @@ -76,7 +76,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME, COLUMN_TYPE AS TYPE, COLUMN_KEY AS `KEY`, IS_NULLABLE AS `NULLABLE`, COLUMN_DEFAULT AS `DEFAULT`, EXTRA AS EXTRA, COLUMN_COMMENT AS COMMENT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '?' AND TABLE_NAME = '?'"; + private static final String SQL_GET_FIELDS = "SELECT COLUMN_NAME AS NAME, DATA_TYPE AS TYPE, IS_NULLABLE AS NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '?' AND TABLE_NAME = '?'"; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/sqlserver/SQLServerResourceBrowser.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/sqlserver/SQLServerResourceBrowser.java index e4888bc..dd8a391 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/sqlserver/SQLServerResourceBrowser.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/sqlserver/SQLServerResourceBrowser.java @@ -64,7 +64,7 @@ public List getSchemas() throws SQLException { return this.getSchemas(SQL.of(SQL_GET_SCHEMAS)); } - private static final String SQL_GET_TABLES = " SELECT table_name AS name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = '?'"; + private static final String SQL_GET_TABLES = " SELECT table_name AS name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = '?' AND table_type = 'BASE TABLE'"; @Override public List
getTables(String schema) throws SQLException { @@ -78,7 +78,7 @@ public List getViews(String schema) throws SQLException { return this.getViews(SQL.of(SQL_GET_VIEWS, schema)); } - private static final String SQL_GET_FIELDS = "SELECT column_name AS name, column_type AS type, column_key AS `key`, is_nullable AS `nullable`, column_default AS `default`, extra AS extra, column_comment AS comment FROM information_schema.columns WHERE table_schema = '?' AND table_name = '?'"; + private static final String SQL_GET_FIELDS = "SELECT column_name AS name, data_type AS type, is_nullable AS nullable FROM information_schema.columns WHERE table_schema = '?' AND table_name = '?'"; @Override public List getFields(String schema, String table) throws SQLException { diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/controller/ResourceController.java b/backend/web/src/main/java/org/jumpserver/chen/web/controller/ResourceController.java index 1a5bc4f..9b925e9 100644 --- a/backend/web/src/main/java/org/jumpserver/chen/web/controller/ResourceController.java +++ b/backend/web/src/main/java/org/jumpserver/chen/web/controller/ResourceController.java @@ -5,9 +5,14 @@ import org.jumpserver.chen.framework.datasource.entity.action.EventEmitter; import org.jumpserver.chen.framework.datasource.entity.form.FormData; import org.jumpserver.chen.framework.datasource.entity.resource.TreeNode; +import org.jumpserver.chen.framework.datasource.metadata.RelationMetadataPage; import org.jumpserver.chen.framework.session.SessionManager; import org.jumpserver.chen.web.entity.GetHintsRequest; +import org.jumpserver.chen.web.entity.MetadataColumnsRequest; +import org.jumpserver.chen.web.entity.MetadataColumnsResponse; +import org.jumpserver.chen.web.entity.MetadataRelationsRequest; import org.jumpserver.chen.web.service.ResourceService; +import org.jumpserver.chen.web.service.SqlMetadataService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -23,6 +28,9 @@ public class ResourceController { @Autowired private ResourceService resourceService; + @Autowired + private SqlMetadataService sqlMetadataService; + @PostMapping("/children") public List getChild(@RequestBody(required = false) TreeNode node, @@ -61,4 +69,16 @@ public Map> getHints(@RequestBody GetHintsRequest request) .getHints(request.getNodeKey(), request.getContext()); } + @PostMapping("/metadata/relations") + public RelationMetadataPage getMetadataRelations( + @RequestBody MetadataRelationsRequest request + ) { + return this.sqlMetadataService.listRelations(request); + } + + @PostMapping("/metadata/columns") + public MetadataColumnsResponse getMetadataColumns(@RequestBody MetadataColumnsRequest request) { + return this.sqlMetadataService.listColumns(request); + } + } diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataColumnsRequest.java b/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataColumnsRequest.java new file mode 100644 index 0000000..9bb7135 --- /dev/null +++ b/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataColumnsRequest.java @@ -0,0 +1,13 @@ +package org.jumpserver.chen.web.entity; + +import lombok.Data; +import org.jumpserver.chen.framework.datasource.metadata.QualifiedRelation; + +import java.util.List; + +@Data +public class MetadataColumnsRequest { + private String nodeKey; + private String context; + private List relations; +} diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataColumnsResponse.java b/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataColumnsResponse.java new file mode 100644 index 0000000..7478b0d --- /dev/null +++ b/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataColumnsResponse.java @@ -0,0 +1,8 @@ +package org.jumpserver.chen.web.entity; + +import org.jumpserver.chen.framework.datasource.metadata.RelationColumnsMetadata; + +import java.util.List; + +public record MetadataColumnsResponse(List items) { +} diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataRelationsRequest.java b/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataRelationsRequest.java new file mode 100644 index 0000000..f56e9c3 --- /dev/null +++ b/backend/web/src/main/java/org/jumpserver/chen/web/entity/MetadataRelationsRequest.java @@ -0,0 +1,11 @@ +package org.jumpserver.chen.web.entity; + +import lombok.Data; + +@Data +public class MetadataRelationsRequest { + private String nodeKey; + private String context; + private String prefix; + private Integer limit; +} diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/service/SqlMetadataService.java b/backend/web/src/main/java/org/jumpserver/chen/web/service/SqlMetadataService.java new file mode 100644 index 0000000..3e39365 --- /dev/null +++ b/backend/web/src/main/java/org/jumpserver/chen/web/service/SqlMetadataService.java @@ -0,0 +1,67 @@ +package org.jumpserver.chen.web.service; + +import org.apache.commons.lang3.StringUtils; +import org.jumpserver.chen.framework.datasource.ResourceBrowser; +import org.jumpserver.chen.framework.datasource.entity.resource.ResourceNodeSnapshot; +import org.jumpserver.chen.framework.datasource.metadata.RelationMetadataPage; +import org.jumpserver.chen.framework.datasource.metadata.SqlMetadataCatalog; +import org.jumpserver.chen.framework.session.SessionManager; +import org.jumpserver.chen.web.entity.MetadataColumnsRequest; +import org.jumpserver.chen.web.entity.MetadataColumnsResponse; +import org.jumpserver.chen.web.entity.MetadataRelationsRequest; +import org.jumpserver.chen.web.exception.ChenException; +import org.springframework.stereotype.Service; + +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +@Service +public class SqlMetadataService { + private static final Set QUERY_NODE_TYPES = Set.of("datasource", "database", "schema", "table"); + + public RelationMetadataPage listRelations(MetadataRelationsRequest request) { + var session = SessionManager.getCurrentSession(); + if (!session.enableAutoComplete()) { + return new RelationMetadataPage(List.of(), false); + } + + var datasource = session.getDatasource(); + var browser = datasource.getResourceBrowser(); + var node = this.resolveNode(browser, request == null ? null : request.getNodeKey()); + var catalog = new SqlMetadataCatalog(browser, datasource.getConnectionManager()); + try { + return catalog.listRelations(node, request.getContext(), request.getPrefix(), request.getLimit()); + } catch (SQLException | IllegalArgumentException e) { + throw new ChenException("Failed to load SQL relation metadata", e); + } + } + + public MetadataColumnsResponse listColumns(MetadataColumnsRequest request) { + var session = SessionManager.getCurrentSession(); + if (!session.enableAutoComplete()) { + return new MetadataColumnsResponse(List.of()); + } + + var datasource = session.getDatasource(); + var browser = datasource.getResourceBrowser(); + var node = this.resolveNode(browser, request == null ? null : request.getNodeKey()); + var catalog = new SqlMetadataCatalog(browser, datasource.getConnectionManager()); + try { + return new MetadataColumnsResponse(catalog.listColumns(node, request.getContext(), request.getRelations())); + } catch (SQLException | IllegalArgumentException e) { + throw new ChenException("Failed to load SQL column metadata", e); + } + } + + private ResourceNodeSnapshot resolveNode(ResourceBrowser browser, String nodeKey) { + if (StringUtils.isBlank(nodeKey)) { + throw new ChenException("Invalid metadata context"); + } + var node = browser.getIndexedNode(nodeKey); + if (node == null || !QUERY_NODE_TYPES.contains(node.type())) { + throw new ChenException("Invalid metadata context"); + } + return node; + } +}