Skip to content

Commit 4b10013

Browse files
committed
feat: add lazy CodeMirror SQL completion
1 parent 1e0df20 commit 4b10013

18 files changed

Lines changed: 366 additions & 8 deletions

File tree

backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/ResourceBrowser.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ public interface ResourceBrowser {
1717

1818
List<TreeNode> getChildren(TreeNode node, boolean fromCache) throws SQLException;
1919

20+
List<Schema> getSchemas() throws SQLException;
21+
22+
List<Table> getTables(String schema) throws SQLException;
23+
24+
List<View> getViews(String schema) throws SQLException;
25+
26+
List<Field> getFields(String schema, String table) throws SQLException;
27+
2028
List<Schema> getSchemas(SQL sql) throws SQLException;
2129

2230
List<Table> getTables(SQL sql) throws SQLException;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.jumpserver.chen.framework.datasource.metadata;
2+
3+
public record QualifiedRelation(
4+
String catalog,
5+
String schema,
6+
String name,
7+
String kind
8+
) {
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.jumpserver.chen.framework.datasource.metadata;
2+
3+
import java.util.List;
4+
5+
public record RelationColumnsMetadata(
6+
QualifiedRelation relation,
7+
List<SqlColumnMetadata> columns
8+
) {
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.jumpserver.chen.framework.datasource.metadata;
2+
3+
import java.util.List;
4+
5+
public record RelationMetadataPage(
6+
List<QualifiedRelation> items,
7+
boolean truncated
8+
) {
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.jumpserver.chen.framework.datasource.metadata;
2+
3+
public record SqlColumnMetadata(
4+
String name,
5+
String dataType,
6+
boolean nullable
7+
) {
8+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package org.jumpserver.chen.framework.datasource.metadata;
2+
3+
import org.apache.commons.lang3.StringUtils;
4+
import org.jumpserver.chen.framework.datasource.ConnectionManager;
5+
import org.jumpserver.chen.framework.datasource.ResourceBrowser;
6+
import org.jumpserver.chen.framework.datasource.entity.resource.ResourceNodeSnapshot;
7+
import org.jumpserver.chen.framework.utils.SqlIdentifierUtils;
8+
9+
import java.sql.SQLException;
10+
import java.util.ArrayList;
11+
import java.util.Comparator;
12+
import java.util.LinkedHashMap;
13+
import java.util.List;
14+
import java.util.Locale;
15+
import java.util.Map;
16+
import java.util.Objects;
17+
import java.util.Set;
18+
import java.util.function.Function;
19+
import java.util.stream.Collectors;
20+
21+
public class SqlMetadataCatalog {
22+
public static final int DEFAULT_RELATION_LIMIT = 100;
23+
public static final int MAX_RELATION_LIMIT = 200;
24+
public static final int MAX_COLUMN_RELATIONS = 20;
25+
26+
private static final Set<String> RELATION_KINDS = Set.of("table", "view");
27+
28+
private final ResourceBrowser resourceBrowser;
29+
private final ConnectionManager connectionManager;
30+
31+
public SqlMetadataCatalog(ResourceBrowser resourceBrowser, ConnectionManager connectionManager) {
32+
this.resourceBrowser = Objects.requireNonNull(resourceBrowser);
33+
this.connectionManager = Objects.requireNonNull(connectionManager);
34+
}
35+
36+
public RelationMetadataPage listRelations(ResourceNodeSnapshot node, String context, String prefix, Integer limit)
37+
throws SQLException {
38+
var scope = this.resolveScope(node, context);
39+
var schemas = this.resolveSchemas(scope.schema());
40+
var relations = new ArrayList<QualifiedRelation>();
41+
42+
for (var schema : schemas) {
43+
resourceBrowser.getTables(schema).forEach(table -> relations.add(
44+
new QualifiedRelation(scope.catalog(), schema, table.getName(), "table")
45+
));
46+
resourceBrowser.getViews(schema).forEach(view -> relations.add(
47+
new QualifiedRelation(scope.catalog(), schema, view.getName(), "view")
48+
));
49+
}
50+
51+
var normalizedPrefix = StringUtils.defaultString(prefix).trim().toLowerCase(Locale.ROOT);
52+
var filtered = relations.stream()
53+
.filter(relation -> normalizedPrefix.isEmpty()
54+
|| relation.name().toLowerCase(Locale.ROOT).startsWith(normalizedPrefix)
55+
|| (relation.schema() + "." + relation.name()).toLowerCase(Locale.ROOT)
56+
.startsWith(normalizedPrefix))
57+
.sorted(Comparator.comparing(QualifiedRelation::schema, String.CASE_INSENSITIVE_ORDER)
58+
.thenComparing(QualifiedRelation::name, String.CASE_INSENSITIVE_ORDER)
59+
.thenComparing(QualifiedRelation::kind))
60+
.toList();
61+
62+
var boundedLimit = limit == null
63+
? DEFAULT_RELATION_LIMIT
64+
: Math.max(1, Math.min(limit, MAX_RELATION_LIMIT));
65+
return new RelationMetadataPage(
66+
List.copyOf(filtered.subList(0, Math.min(filtered.size(), boundedLimit))),
67+
filtered.size() > boundedLimit
68+
);
69+
}
70+
71+
public List<RelationColumnsMetadata> listColumns(
72+
ResourceNodeSnapshot node,
73+
String context,
74+
List<QualifiedRelation> requestedRelations
75+
) throws SQLException {
76+
if (requestedRelations == null || requestedRelations.isEmpty()) {
77+
return List.of();
78+
}
79+
if (requestedRelations.size() > MAX_COLUMN_RELATIONS) {
80+
throw new IllegalArgumentException("Too many relations in one metadata request");
81+
}
82+
83+
var scope = this.resolveScope(node, context);
84+
var availableSchemas = this.resolveSchemas(null);
85+
var canonicalSchemas = availableSchemas.stream()
86+
.collect(Collectors.toMap(Function.identity(), Function.identity(), (left, right) -> left));
87+
var relationsBySchema = new LinkedHashMap<String, Map<RelationKey, QualifiedRelation>>();
88+
var canonicalRequests = new LinkedHashMap<RelationKey, QualifiedRelation>();
89+
90+
for (var requested : requestedRelations) {
91+
this.validateRequestedRelation(requested, scope.catalog());
92+
var requestedSchema = StringUtils.defaultIfBlank(requested.schema(), scope.schema());
93+
var canonicalSchema = canonicalSchemas.get(requestedSchema);
94+
if (canonicalSchema == null) {
95+
throw new IllegalArgumentException("Unknown relation schema");
96+
}
97+
98+
var availableRelations = relationsBySchema.get(canonicalSchema);
99+
if (availableRelations == null) {
100+
availableRelations = this.loadRelationsByKey(scope.catalog(), canonicalSchema);
101+
relationsBySchema.put(canonicalSchema, availableRelations);
102+
}
103+
var key = new RelationKey(canonicalSchema, requested.name(), requested.kind());
104+
var canonical = availableRelations.get(key);
105+
if (canonical == null) {
106+
throw new IllegalArgumentException("Unknown relation");
107+
}
108+
canonicalRequests.putIfAbsent(key, canonical);
109+
}
110+
111+
var result = new ArrayList<RelationColumnsMetadata>();
112+
for (var entry : canonicalRequests.entrySet()) {
113+
var relation = entry.getValue();
114+
var columns = resourceBrowser.getFields(relation.schema(), relation.name()).stream()
115+
.map(field -> new SqlColumnMetadata(field.getName(), field.getType(), field.isNullable()))
116+
.toList();
117+
result.add(new RelationColumnsMetadata(relation, columns));
118+
}
119+
return List.copyOf(result);
120+
}
121+
122+
private MetadataScope resolveScope(ResourceNodeSnapshot node, String context) throws SQLException {
123+
if (node == null) {
124+
throw new IllegalArgumentException("Invalid metadata context");
125+
}
126+
127+
var contextKey = connectionManager.getContextKey();
128+
var databaseContextKey = connectionManager.getDatabaseContextKey();
129+
var currentContext = StringUtils.defaultString(context).trim();
130+
var catalog = node.database();
131+
132+
if (StringUtils.equals(contextKey, databaseContextKey) && StringUtils.isNotBlank(currentContext)) {
133+
var allowedContexts = connectionManager.getSqlActuator().getSchemas();
134+
if (!allowedContexts.contains(currentContext)) {
135+
throw new IllegalArgumentException("Unknown database metadata context");
136+
}
137+
catalog = currentContext;
138+
}
139+
if (StringUtils.isNotBlank(catalog)) {
140+
SqlIdentifierUtils.validateDatabaseName(catalog);
141+
connectionManager.setDatabaseContext(catalog);
142+
}
143+
144+
String schema = null;
145+
if (StringUtils.equals(contextKey, "schema")) {
146+
schema = StringUtils.defaultIfBlank(currentContext, node.schema());
147+
if (StringUtils.isNotBlank(catalog) && schema.startsWith(catalog + ".")) {
148+
schema = schema.substring(catalog.length() + 1);
149+
}
150+
} else if (StringUtils.equals(node.database(), catalog)) {
151+
schema = node.schema();
152+
}
153+
return new MetadataScope(catalog, schema);
154+
}
155+
156+
private List<String> resolveSchemas(String requestedSchema) throws SQLException {
157+
var schemas = resourceBrowser.getSchemas().stream().map(schema -> schema.getName()).toList();
158+
if (StringUtils.isBlank(requestedSchema)) {
159+
return schemas;
160+
}
161+
return schemas.stream()
162+
.filter(schema -> schema.equals(requestedSchema))
163+
.findFirst()
164+
.map(List::of)
165+
.orElseThrow(() -> new IllegalArgumentException("Unknown metadata schema"));
166+
}
167+
168+
private Map<RelationKey, QualifiedRelation> loadRelationsByKey(String catalog, String schema) throws SQLException {
169+
var result = new LinkedHashMap<RelationKey, QualifiedRelation>();
170+
resourceBrowser.getTables(schema).forEach(table -> {
171+
var relation = new QualifiedRelation(catalog, schema, table.getName(), "table");
172+
result.put(new RelationKey(schema, relation.name(), relation.kind()), relation);
173+
});
174+
resourceBrowser.getViews(schema).forEach(view -> {
175+
var relation = new QualifiedRelation(catalog, schema, view.getName(), "view");
176+
result.put(new RelationKey(schema, relation.name(), relation.kind()), relation);
177+
});
178+
return result;
179+
}
180+
181+
private void validateRequestedRelation(QualifiedRelation relation, String catalog) {
182+
if (relation == null || StringUtils.isBlank(relation.name()) || !RELATION_KINDS.contains(relation.kind())) {
183+
throw new IllegalArgumentException("Invalid relation metadata request");
184+
}
185+
if (StringUtils.isNotBlank(relation.catalog()) && StringUtils.isNotBlank(catalog)
186+
&& !relation.catalog().equals(catalog)) {
187+
throw new IllegalArgumentException("Relation catalog does not match the active context");
188+
}
189+
}
190+
191+
private record MetadataScope(String catalog, String schema) {
192+
}
193+
194+
private record RelationKey(String schema, String name, String kind) {
195+
}
196+
}

backend/modules/src/main/java/org.jumpserver.chen.modules/clickhouse/ClickhouseResourceBrowser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public List<View> getViews(String schema) throws SQLException {
3838
return this.getViews(SQL.of(SQL_GET_VIEWS, schema));
3939
}
4040

41-
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 = '?'";
41+
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 = '?'";
4242

4343
@Override
4444
public List<Field> getFields(String schema, String table) throws SQLException {

backend/modules/src/main/java/org.jumpserver.chen.modules/dameng/DMResourceBrowser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public List<View> getViews(String schema) throws SQLException {
3838
return this.getViews(SQL.of(SQL_GET_VIEWS, schema));
3939
}
4040

41-
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='?'";
41+
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='?'";
4242

4343
@Override
4444
public List<Field> getFields(String schema, String table) throws SQLException {

backend/modules/src/main/java/org.jumpserver.chen.modules/db2/DB2ResourceBrowser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public List<View> getViews(String schema) throws SQLException {
3838
return this.getViews(SQL.of(SQL_GET_VIEWS, schema));
3939
}
4040

41-
private static final String SQL_GET_FIELDS = "select COLNAME, TYPENAME, LENGTH, DEFAULT, REMARKS from syscat.COLUMNS where TABSCHEMA = '?' and TABNAME = '?'";
41+
private static final String SQL_GET_FIELDS = "select COLNAME, TYPENAME, NULLS from syscat.COLUMNS where TABSCHEMA = '?' and TABNAME = '?'";
4242

4343
@Override
4444
public List<Field> getFields(String schema, String table) throws SQLException {

backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlResourceBrowser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public List<View> getViews(String schema) throws SQLException {
3838
return this.getViews(SQL.of(SQL_GET_VIEWS, schema));
3939
}
4040

41-
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 = '?'";
41+
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 = '?'";
4242

4343
@Override
4444
public List<Field> getFields(String schema, String table) throws SQLException {

0 commit comments

Comments
 (0)