Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ extend type Query {
"Returns list of all available entity query generators"
sqlEntityQueryGenerators(nodePathList: [String!]!): [SQLQueryGenerator!]!

"Returns SQL query generators applicable to the specified result set"
sqlResultSetQueryGenerators(
projectId: ID,
connectionId: ID!,
contextId: ID!,
resultsId: ID!
): [SQLQueryGenerator!]! @since(version: "26.2.0")

"""
Generates SQL query for the specified entity query generator.
"""
Expand All @@ -415,7 +423,22 @@ extend type Query {
resultsId: ID!,
selectedRows: [SQLResultRow!]!,
generatorOptions: SQLQueryGeneratorOptions
): String!
): String! @deprecated(reason: "use sqlGenerateResultSetQueryByGenerator (26.2.0)")

"""
Returns an auto-generated SQL query based on provided rows data.
Accepts any generator id returned by sqlResultSetQueryGenerators and
validates that the generator is applicable to the specified result set.
"""
sqlGenerateResultSetQueryByGenerator(
projectId: ID,
connectionId: ID!,
contextId: ID!,
generatorId: String!,
resultsId: ID!,
selectedRows: [SQLResultRow!]!,
generatorOptions: SQLQueryGeneratorOptions
): String! @since(version: "26.2.0")

"Parses SQL script and returns script info with queries start and end positions"
sqlParseScript(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ SQLGeneratorDescriptor[] getEntityQueryGenerators(
@NotNull WebSession session,
@NotNull List<String> nodePathList) throws DBWebException;

@NotNull
@WebAction
SQLGeneratorDescriptor[] getResultSetQueryGenerators(
@NotNull WebSession session,
@NotNull WebSQLContextInfo sqlContext,
@NotNull String resultsId) throws DBWebException;

@NotNull
@WebAction
String generateEntityQuery(
Expand All @@ -92,7 +99,7 @@ WebAsyncTaskInfo asyncGenerateEntityQuery(

@NotNull
@WebAction
String sqlGenerateResultSetQuery(
String sqlGenerateResultSetQueryByGenerator(
@NotNull WebSession session,
@NotNull WebSQLContextInfo sqlContext,
@NotNull String generatorId,
Expand All @@ -101,6 +108,7 @@ String sqlGenerateResultSetQuery(
@NotNull WebSQLGeneratorOptions options
) throws DBWebException;

@NotNull
@WebAction
WebSQLContextInfo createContext(
@NotNull WebSQLProcessor processor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public void bindWiring(DBWBindingContext model) throws DBWebException {
getWebSession(env),
getArgumentVal(env, "nodePathList"))
)
.dataFetcher("sqlResultSetQueryGenerators", env ->
getService(env).getResultSetQueryGenerators(
getWebSession(env),
getSQLContext(env),
getArgumentVal(env, "resultsId"))
)
.dataFetcher("sqlGenerateEntityQuery", env ->
getService(env).generateEntityQuery(
getWebSession(env),
Expand All @@ -103,7 +109,17 @@ public void bindWiring(DBWBindingContext model) throws DBWebException {
)
)
.dataFetcher("sqlGenerateResultSetQuery", env ->
getService(env).sqlGenerateResultSetQuery(
getService(env).sqlGenerateResultSetQueryByGenerator(
getWebSession(env),
getSQLContext(env),
getArgumentVal(env, "generatorId"),
getArgumentVal(env, "resultsId"),
getResultsRow(env, "selectedRows"),
getGeneratorOptions(env)
)
)
.dataFetcher("sqlGenerateResultSetQueryByGenerator", env ->
getService(env).sqlGenerateResultSetQueryByGenerator(
getWebSession(env),
getSQLContext(env),
getArgumentVal(env, "generatorId"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,14 +230,27 @@ public DBCLogicalOperator[] getSupportedOperations(@NotNull WebSQLContextInfo co
return attribute.getValueHandler().getSupportedOperators(attribute);
}

@NotNull
@Override
public SQLGeneratorDescriptor[] getEntityQueryGenerators(
@NotNull WebSession session,
@NotNull List<String> nodePathList)
throws DBWebException
{
@NotNull List<String> nodePathList
) throws DBWebException {
List<DBSObject> objectList = getObjectListFromNodeIds(session, nodePathList);
return SQLGeneratorConfigurationRegistry.getInstance().getApplicableGenerators(objectList, session).toArray(new SQLGeneratorDescriptor[0]);
return SQLGeneratorConfigurationRegistry.getInstance()
.getApplicableGenerators(objectList, session)
.toArray(new SQLGeneratorDescriptor[0]);
}

@NotNull
@Override
public SQLGeneratorDescriptor[] getResultSetQueryGenerators(
@NotNull WebSession session,
@NotNull WebSQLContextInfo sqlContext,
@NotNull String resultsId
) throws DBWebException {
return getApplicableResultSetGenerators(session, sqlContext, resultsId, Collections.emptyList())
.toArray(new SQLGeneratorDescriptor[0]);
}

@NotNull
Expand Down Expand Up @@ -280,19 +293,49 @@ public void run(@NotNull DBRProgressMonitor monitor) throws InvocationTargetExce

@NotNull
@Override
public String sqlGenerateResultSetQuery(
public String sqlGenerateResultSetQueryByGenerator(
@NotNull WebSession webSession,
@NotNull WebSQLContextInfo sqlContext,
@NotNull String generatorId,
@NotNull String resultsId,
@NotNull List<WebSQLResultsRow> selectedRows,
@NotNull WebSQLGeneratorOptions options
) throws DBWebException {
if (selectedRows.isEmpty()) {
throw new DBWebException("At least one row must be selected");
}
List<SQLGeneratorDescriptor> applicableGenerators = getApplicableResultSetGenerators(
webSession,
sqlContext,
resultsId,
selectedRows
);
if (applicableGenerators.stream().noneMatch(generator -> generator.getId().equals(generatorId))) {
throw new DBWebException("Generator '" + generatorId + "' is not applicable to this result set");
}
checkAndFillTruncatedData(sqlContext, resultsId, selectedRows);
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows);
return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider), options);
}

@NotNull
private List<SQLGeneratorDescriptor> getApplicableResultSetGenerators(
@NotNull WebSession session,
@NotNull WebSQLContextInfo sqlContext,
@NotNull String resultsId,
@NotNull List<WebSQLResultsRow> selectedRows
) throws DBWebException {
sqlContext.getResults(resultsId);
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows);
if (dataProvider.getSingleSource() == null || dataProvider.getAttributes().length == 0) {
return Collections.emptyList();
}
return SQLGeneratorConfigurationRegistry.getInstance().getApplicableGenerators(
Collections.singletonList(dataProvider),
session
);
}

private void checkAndFillTruncatedData(
@NotNull WebSQLContextInfo sqlContext,
@NotNull String resultsId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.cloudbeaver.test.platform.CloudbeaverDBTest;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -51,6 +52,38 @@ public class GenerateSQLResultSetTest extends CloudbeaverDBTest {
}
""";

private static final String GQL_GENERATE_QUERY_BY_GENERATOR = """
query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: String!,
$resultsId: ID!, $selectedRows: [SQLResultRow!]!
) {
sqlGenerateResultSetQueryByGenerator(
projectId: $projectId
connectionId: $connectionId
contextId: $contextId
generatorId: $generatorId
resultsId: $resultsId
selectedRows: $selectedRows
)
}
""";

private static final String GQL_RESULT_SET_QUERY_GENERATORS = """
query($projectId: ID, $connectionId: ID!, $contextId: ID!, $resultsId: ID!) {
sqlResultSetQueryGenerators(
projectId: $projectId
connectionId: $connectionId
contextId: $contextId
resultsId: $resultsId
) {
id
label
description
order
multiObject
}
}
""";

private List<Map<String, Object>> selectedRows;
private WebSQLContextInfo sqlProcessorContext;
private String resultId;
Expand Down Expand Up @@ -86,7 +119,7 @@ public void prepareTables() throws Exception {
}

@Test
public void shouldGenerateSelectQueryFromResultSet() throws Exception {
public void shouldGenerateSelectQueryFromResultSetWithLegacyGeneratorId() throws Exception {
// When
String query = generateQuery("dataSelect", selectedRows);

Expand All @@ -101,6 +134,102 @@ public void shouldGenerateSelectQueryFromResultSet() throws Exception {
);
}

@Test
public void shouldDiscoverApplicableResultSetGeneratorsInOrder() throws Exception {
Map<String, Object> response = client.executeGQLRequest(
GQL_RESULT_SET_QUERY_GENERATORS,
getBaseVariables(),
Map.of(),
"sqlResultSetQueryGenerators"
);
List<Map<String, Object>> generators = (List<Map<String, Object>>) response.get("data");

List<Object> generatorIds = generators.stream().map(generator -> generator.get("id")).toList();
Assertions.assertTrue(generatorIds.containsAll(
List.of("dataSelect", "dataSelectMany", "dataInsert", "dataUpdate", "dataDeleteByUniqueKey")
));
List<Integer> generatorOrders = generators.stream()
.map(generator -> ((Number) generator.get("order")).intValue())
.toList();
Assertions.assertEquals(
generatorOrders.stream().sorted().toList(),
generatorOrders
);
Assertions.assertFalse(generatorIds.contains("tableDDL"));
}

@Test
public void shouldGenerateQueryByDiscoveredGenerator() throws Exception {
String query = generateQueryByGenerator("dataSelectMany", selectedRows);

Assertions.assertEquals("""
SELECT ID, FIELD
FROM PUBLIC.TEST_TABLE
WHERE ID IN (1,2);""", query);
}

@Test
public void shouldRejectInapplicableGenerator() {
DBException exception = Assertions.assertThrows(
DBException.class,
() -> generateQueryByGenerator("tableDDL", selectedRows)
);

Assertions.assertTrue(exception.getMessage().contains("not applicable to this result set"));
}

@Test
public void shouldRejectEmptySelectedRows() {
DBException exception = Assertions.assertThrows(
DBException.class,
() -> generateQueryByGenerator("dataSelect", List.of())
);

Assertions.assertTrue(exception.getMessage().contains("At least one row must be selected"));
}

@Test
public void shouldRejectLegacyGeneratorIdNotApplicableToResultSet() throws Exception {
String taskId = clientWrapper.asyncSqlExecute(
globalProject,
sqlProcessorContext,
databaseContainer.getId(),
"SELECT 1 AS VALUE"
);
clientWrapper.waitTaskCompleted(taskId);
Map<String, Object> resultSet = clientWrapper.readTaskResultSet(taskId);
resultId = resultSet.get("id").toString();
List<Map<String, Object>> rows = JSONUtils.getObjectList(resultSet, "rowsWithMetaData");

DBException exception = Assertions.assertThrows(
DBException.class,
() -> generateQuery("dataSelect", rows)
);

Assertions.assertTrue(exception.getMessage().contains("not applicable to this result set"));
}

@Test
public void shouldNotDiscoverGeneratorsWithoutSingleSource() throws Exception {
String taskId = clientWrapper.asyncSqlExecute(
globalProject,
sqlProcessorContext,
databaseContainer.getId(),
"SELECT 1 AS VALUE"
);
clientWrapper.waitTaskCompleted(taskId);
resultId = clientWrapper.readTaskResultSet(taskId).get("id").toString();

Map<String, Object> response = client.executeGQLRequest(
GQL_RESULT_SET_QUERY_GENERATORS,
getBaseVariables(),
Map.of(),
"sqlResultSetQueryGenerators"
);

Assertions.assertEquals(List.of(), response.get("data"));
}

@Test
public void shouldGenerateSelectQueryWithoutFullyQualifiedNames() throws Exception {
// When
Expand Down Expand Up @@ -288,12 +417,8 @@ private String generateQuery(
@NotNull String gqlQuery,
@Nullable Map<String, Object> variables
) throws Exception {
Map<String, Object> queryVariables = new HashMap<>();
queryVariables.put("projectId", globalProject.getId());
queryVariables.put("connectionId", databaseContainer.getId());
queryVariables.put("contextId", sqlProcessorContext.getId());
Map<String, Object> queryVariables = getBaseVariables();
queryVariables.put("generatorId", generatorId);
queryVariables.put("resultsId", resultId);
queryVariables.put("selectedRows", selectedRows);
if (variables != null && !variables.isEmpty()) {
queryVariables.putAll(variables);
Expand All @@ -305,4 +430,31 @@ private String generateQuery(
return response.get("data")
.toString();
}

@Nullable
private String generateQueryByGenerator(
@NotNull String generatorId,
@NotNull List<Map<String, Object>> selectedRows
) throws Exception {
Map<String, Object> queryVariables = getBaseVariables();
queryVariables.put("generatorId", generatorId);
queryVariables.put("selectedRows", selectedRows);
Map<String, Object> response = client.executeGQLRequest(
GQL_GENERATE_QUERY_BY_GENERATOR, queryVariables,
Map.of(), "sqlGenerateResultSetQueryByGenerator"
);
return response.get("data")
.toString();
}

@NotNull
private Map<String, Object> getBaseVariables() {
Map<String, Object> queryVariables = new HashMap<>();
queryVariables.put("projectId", globalProject.getId());
queryVariables.put("connectionId", databaseContainer.getId());
queryVariables.put("contextId", sqlProcessorContext.getId());
queryVariables.put("resultsId", resultId);
return queryVariables;
}

}
Loading