From b8a737f2dd19ec249ec44291033f51e6912ef682 Mon Sep 17 00:00:00 2001 From: "daniil.grigorev" Date: Wed, 19 Aug 2026 11:52:02 +0200 Subject: [PATCH 1/6] dbeaver/pro#9236 [CB] Generate SQL - throw actual error for drivers which not support DDL structure. Backend part --- .../schema/service.sql.graphqls | 23 +- .../service/sql/DBWServiceSQL.java | 9 +- .../service/sql/WebServiceBindingSQL.java | 9 +- .../service/sql/impl/WebServiceSQL.java | 49 ++++- .../sql/GenerateSQLResultSetTest.java | 197 +++++++++++++++++- 5 files changed, 269 insertions(+), 18 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 99cf26426ea..ba6c671cab7 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -187,15 +187,15 @@ enum SQLRowIdentifierState { enum SQLResultSetGeneratorId { "SELECT .. WHERE .. =" - dataSelect, + dataSelect @deprecated(reason: "use generatorKey (26.2.0)"), "SELECT .. WHERE .. IN" - dataSelectMany, + dataSelectMany @deprecated(reason: "use generatorKey (26.2.0)"), "INSERT" - dataInsert, + dataInsert @deprecated(reason: "use generatorKey (26.2.0)"), "UPDATE" - dataUpdate, + dataUpdate @deprecated(reason: "use generatorKey (26.2.0)"), "DELETE by Unique Key" - dataDeleteByUniqueKey, + dataDeleteByUniqueKey @deprecated(reason: "use generatorKey (26.2.0)"), } type SQLRowIdentifier { @@ -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. """ @@ -405,13 +413,14 @@ extend type Query { """ Returns an auto-generated SQL query based on provided rows data. - Uses generatorId value to establish the type of query (e.g. SELECT or INSERT) + Uses the requested generator identifier to establish the type of query (e.g. SELECT or INSERT) """ sqlGenerateResultSetQuery( projectId: ID, connectionId: ID!, contextId: ID!, - generatorId: SQLResultSetGeneratorId!, + generatorId: SQLResultSetGeneratorId @deprecated(reason: "use generatorKey (26.2.0)"), + generatorKey: String @since(version: "26.2.0"), resultsId: ID!, selectedRows: [SQLResultRow!]!, generatorOptions: SQLQueryGeneratorOptions diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 9be7270c05c..7c7f2b13e33 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -72,6 +72,12 @@ SQLGeneratorDescriptor[] getEntityQueryGenerators( @NotNull WebSession session, @NotNull List nodePathList) throws DBWebException; + @WebAction + SQLGeneratorDescriptor[] getResultSetQueryGenerators( + @NotNull WebSession session, + @NotNull WebSQLContextInfo sqlContext, + @NotNull String resultsId) throws DBWebException; + @NotNull @WebAction String generateEntityQuery( @@ -95,7 +101,8 @@ WebAsyncTaskInfo asyncGenerateEntityQuery( String sqlGenerateResultSetQuery( @NotNull WebSession session, @NotNull WebSQLContextInfo sqlContext, - @NotNull String generatorId, + @Nullable String generatorId, + @Nullable String generatorKey, @NotNull String resultsId, @NotNull List selectedRows, @NotNull WebSQLGeneratorOptions options diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index a6a3adb0ebd..42ad846792a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -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), @@ -106,7 +112,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getService(env).sqlGenerateResultSetQuery( getWebSession(env), getSQLContext(env), - getArgumentVal(env, "generatorId"), + getArgument(env, "generatorId"), + getArgument(env, "generatorKey"), getArgumentVal(env, "resultsId"), getResultsRow(env, "selectedRows"), getGeneratorOptions(env) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 4f6d6ad50d1..3af10ae9009 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -240,6 +240,16 @@ public SQLGeneratorDescriptor[] getEntityQueryGenerators( return SQLGeneratorConfigurationRegistry.getInstance().getApplicableGenerators(objectList, session).toArray(new SQLGeneratorDescriptor[0]); } + @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 @Override public String generateEntityQuery( @@ -283,14 +293,49 @@ public void run(@NotNull DBRProgressMonitor monitor) throws InvocationTargetExce public String sqlGenerateResultSetQuery( @NotNull WebSession webSession, @NotNull WebSQLContextInfo sqlContext, - @NotNull String generatorId, + @Nullable String generatorId, + @Nullable String generatorKey, @NotNull String resultsId, @NotNull List selectedRows, @NotNull WebSQLGeneratorOptions options ) throws DBWebException { + if ((generatorId == null) == (generatorKey == null)) { + throw new DBWebException("Exactly one of generatorId or generatorKey must be specified"); + } + if (selectedRows.isEmpty()) { + throw new DBWebException("At least one row must be selected"); + } + String requestedGeneratorId = generatorKey != null ? generatorKey : generatorId; + List applicableGenerators = getApplicableResultSetGenerators( + webSession, + sqlContext, + resultsId, + selectedRows + ); + if (applicableGenerators.stream().noneMatch(generator -> generator.getId().equals(requestedGeneratorId))) { + throw new DBWebException("Generator '" + requestedGeneratorId + "' 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); + return createAndRunGenerator(webSession, requestedGeneratorId, Collections.singletonList(dataProvider), options); + } + + @NotNull + private List getApplicableResultSetGenerators( + @NotNull WebSession session, + @NotNull WebSQLContextInfo sqlContext, + @NotNull String resultsId, + @NotNull List 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( diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java index fc29336c4c1..190b8466f96 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java @@ -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; @@ -51,6 +52,54 @@ public class GenerateSQLResultSetTest extends CloudbeaverDBTest { } """; + private static final String GQL_GENERATE_QUERY_FROM_RESULTSET_BY_KEY = """ + query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorKey: String!, + $resultsId: ID!, $selectedRows: [SQLResultRow!]! + ) { + sqlGenerateResultSetQuery( + projectId: $projectId + connectionId: $connectionId + contextId: $contextId + generatorKey: $generatorKey + 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 static final String GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS = """ + query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: SQLResultSetGeneratorId, + $generatorKey: String, $resultsId: ID!, $selectedRows: [SQLResultRow!]! + ) { + sqlGenerateResultSetQuery( + projectId: $projectId + connectionId: $connectionId + contextId: $contextId + generatorId: $generatorId + generatorKey: $generatorKey + resultsId: $resultsId + selectedRows: $selectedRows + ) + } + """; + private List> selectedRows; private WebSQLContextInfo sqlProcessorContext; private String resultId; @@ -86,7 +135,7 @@ public void prepareTables() throws Exception { } @Test - public void shouldGenerateSelectQueryFromResultSet() throws Exception { + public void shouldGenerateSelectQueryFromResultSetWithLegacyGeneratorId() throws Exception { // When String query = generateQuery("dataSelect", selectedRows); @@ -101,6 +150,108 @@ public void shouldGenerateSelectQueryFromResultSet() throws Exception { ); } + @Test + public void shouldDiscoverApplicableResultSetGeneratorsInOrder() throws Exception { + Map response = client.executeGQLRequest( + GQL_RESULT_SET_QUERY_GENERATORS, + getBaseVariables(), + Map.of(), + "sqlResultSetQueryGenerators" + ); + List> generators = (List>) response.get("data"); + + List generatorIds = generators.stream().map(generator -> generator.get("id")).toList(); + Assertions.assertTrue(generatorIds.containsAll( + List.of("dataSelect", "dataSelectMany", "dataInsert", "dataUpdate", "dataDeleteByUniqueKey") + )); + List 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 shouldGenerateQueryByDiscoveredGeneratorKey() throws Exception { + String query = generateQuery("dataSelectMany", selectedRows, GQL_GENERATE_QUERY_FROM_RESULTSET_BY_KEY, null, "generatorKey"); + + Assertions.assertEquals(""" + SELECT ID, FIELD + FROM PUBLIC.TEST_TABLE + WHERE ID IN (1,2);""", query); + } + + @Test + public void shouldRejectInapplicableGeneratorKey() { + DBException exception = Assertions.assertThrows( + DBException.class, + () -> generateQuery("tableDDL", selectedRows, GQL_GENERATE_QUERY_FROM_RESULTSET_BY_KEY, null, "generatorKey") + ); + + Assertions.assertTrue(exception.getMessage().contains("not applicable to this result set")); + } + + @Test + public void shouldRejectMissingGeneratorArguments() { + DBException exception = Assertions.assertThrows( + DBException.class, + () -> executeGenerationQuery(GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS, Map.of()) + ); + + Assertions.assertTrue(exception.getMessage().contains("Exactly one of generatorId or generatorKey must be specified")); + } + + @Test + public void shouldRejectConflictingGeneratorArguments() { + DBException exception = Assertions.assertThrows( + DBException.class, + () -> executeGenerationQuery( + GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS, + Map.of("generatorId", "dataSelect", "generatorKey", "dataSelect") + ) + ); + + Assertions.assertTrue(exception.getMessage().contains("Exactly one of generatorId or generatorKey must be specified")); + } + + @Test + public void shouldRejectEmptySelectedRows() { + DBException exception = Assertions.assertThrows( + DBException.class, + () -> executeGenerationQuery( + GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS, + Map.of("generatorKey", "dataSelect"), + List.of() + ) + ); + + Assertions.assertTrue(exception.getMessage().contains("At least one row must be selected")); + } + + @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 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 @@ -288,12 +439,19 @@ private String generateQuery( @NotNull String gqlQuery, @Nullable Map variables ) throws Exception { - Map queryVariables = new HashMap<>(); - queryVariables.put("projectId", globalProject.getId()); - queryVariables.put("connectionId", databaseContainer.getId()); - queryVariables.put("contextId", sqlProcessorContext.getId()); - queryVariables.put("generatorId", generatorId); - queryVariables.put("resultsId", resultId); + return generateQuery(generatorId, selectedRows, gqlQuery, variables, "generatorId"); + } + + @Nullable + private String generateQuery( + @NotNull String generatorId, + @NotNull List> selectedRows, + @NotNull String gqlQuery, + @Nullable Map variables, + @NotNull String generatorArgument + ) throws Exception { + Map queryVariables = getBaseVariables(); + queryVariables.put(generatorArgument, generatorId); queryVariables.put("selectedRows", selectedRows); if (variables != null && !variables.isEmpty()) { queryVariables.putAll(variables); @@ -305,4 +463,29 @@ private String generateQuery( return response.get("data") .toString(); } + + @NotNull + private Map getBaseVariables() { + Map queryVariables = new HashMap<>(); + queryVariables.put("projectId", globalProject.getId()); + queryVariables.put("connectionId", databaseContainer.getId()); + queryVariables.put("contextId", sqlProcessorContext.getId()); + queryVariables.put("resultsId", resultId); + return queryVariables; + } + + private void executeGenerationQuery(@NotNull String query, @NotNull Map generatorVariables) throws Exception { + executeGenerationQuery(query, generatorVariables, selectedRows); + } + + private void executeGenerationQuery( + @NotNull String query, + @NotNull Map generatorVariables, + @NotNull List> rows + ) throws Exception { + Map variables = getBaseVariables(); + variables.put("selectedRows", rows); + variables.putAll(generatorVariables); + client.executeGQLRequest(query, variables, Map.of(), "sqlGenerateResultSetQuery"); + } } From ef6c084ef7226508c9f04d10bce4dcfd6248403a Mon Sep 17 00:00:00 2001 From: "daniil.grigorev" Date: Wed, 19 Aug 2026 11:58:31 +0200 Subject: [PATCH 2/6] dbeaver/pro#9236 backend, linter fix --- .../src/io/cloudbeaver/service/sql/DBWServiceSQL.java | 1 + .../src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 7c7f2b13e33..69ece156469 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -72,6 +72,7 @@ SQLGeneratorDescriptor[] getEntityQueryGenerators( @NotNull WebSession session, @NotNull List nodePathList) throws DBWebException; + @NotNull @WebAction SQLGeneratorDescriptor[] getResultSetQueryGenerators( @NotNull WebSession session, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 3af10ae9009..5dd7ee8a5e3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -237,9 +237,12 @@ public SQLGeneratorDescriptor[] getEntityQueryGenerators( throws DBWebException { List 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, From 2a0cb18e299720ea3abed0364fa8e713e90291ae Mon Sep 17 00:00:00 2001 From: "daniil.grigorev" Date: Wed, 19 Aug 2026 12:21:06 +0200 Subject: [PATCH 3/6] dbeaver/pro#9236 backend, linter fix --- .../src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 5dd7ee8a5e3..a875032932b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -233,9 +233,8 @@ public DBCLogicalOperator[] getSupportedOperations(@NotNull WebSQLContextInfo co @Override public SQLGeneratorDescriptor[] getEntityQueryGenerators( @NotNull WebSession session, - @NotNull List nodePathList) - throws DBWebException - { + @NotNull List nodePathList + ) throws DBWebException { List objectList = getObjectListFromNodeIds(session, nodePathList); return SQLGeneratorConfigurationRegistry.getInstance() .getApplicableGenerators(objectList, session) From a4dddefce2cd5f23e9b26df34478439814c19c5f Mon Sep 17 00:00:00 2001 From: "daniil.grigorev" Date: Fri, 21 Aug 2026 09:49:45 +0200 Subject: [PATCH 4/6] dbeaver/pro#9236 [CB] backend: Generate SQL - add sqlGenerateResultSetQueryByGenerator query, keep legacy sqlGenerateResultSetQuery without changes --- .../schema/service.sql.graphqls | 30 +++-- .../service/sql/DBWServiceSQL.java | 14 ++- .../service/sql/WebServiceBindingSQL.java | 13 ++- .../service/sql/impl/WebServiceSQL.java | 28 +++-- .../sql/GenerateSQLResultSetTest.java | 104 +++++------------- 5 files changed, 90 insertions(+), 99 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index ba6c671cab7..edcfa5db1a7 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -187,15 +187,15 @@ enum SQLRowIdentifierState { enum SQLResultSetGeneratorId { "SELECT .. WHERE .. =" - dataSelect @deprecated(reason: "use generatorKey (26.2.0)"), + dataSelect, "SELECT .. WHERE .. IN" - dataSelectMany @deprecated(reason: "use generatorKey (26.2.0)"), + dataSelectMany, "INSERT" - dataInsert @deprecated(reason: "use generatorKey (26.2.0)"), + dataInsert, "UPDATE" - dataUpdate @deprecated(reason: "use generatorKey (26.2.0)"), + dataUpdate, "DELETE by Unique Key" - dataDeleteByUniqueKey @deprecated(reason: "use generatorKey (26.2.0)"), + dataDeleteByUniqueKey, } type SQLRowIdentifier { @@ -413,19 +413,33 @@ extend type Query { """ Returns an auto-generated SQL query based on provided rows data. - Uses the requested generator identifier to establish the type of query (e.g. SELECT or INSERT) + Uses generatorId value to establish the type of query (e.g. SELECT or INSERT) """ sqlGenerateResultSetQuery( projectId: ID, connectionId: ID!, contextId: ID!, - generatorId: SQLResultSetGeneratorId @deprecated(reason: "use generatorKey (26.2.0)"), - generatorKey: String @since(version: "26.2.0"), + generatorId: SQLResultSetGeneratorId!, resultsId: ID!, selectedRows: [SQLResultRow!]!, generatorOptions: SQLQueryGeneratorOptions ): String! + """ + 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( projectId: ID, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 69ece156469..44fb63a79e5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -102,8 +102,18 @@ WebAsyncTaskInfo asyncGenerateEntityQuery( String sqlGenerateResultSetQuery( @NotNull WebSession session, @NotNull WebSQLContextInfo sqlContext, - @Nullable String generatorId, - @Nullable String generatorKey, + @NotNull String generatorId, + @NotNull String resultsId, + @NotNull List selectedRows, + @NotNull WebSQLGeneratorOptions options + ) throws DBWebException; + + @NotNull + @WebAction + String sqlGenerateResultSetQueryByGenerator( + @NotNull WebSession session, + @NotNull WebSQLContextInfo sqlContext, + @NotNull String generatorId, @NotNull String resultsId, @NotNull List selectedRows, @NotNull WebSQLGeneratorOptions options diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index 42ad846792a..00021a04882 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -112,8 +112,17 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getService(env).sqlGenerateResultSetQuery( getWebSession(env), getSQLContext(env), - getArgument(env, "generatorId"), - getArgument(env, "generatorKey"), + getArgumentVal(env, "generatorId"), + getArgumentVal(env, "resultsId"), + getResultsRow(env, "selectedRows"), + getGeneratorOptions(env) + ) + ) + .dataFetcher("sqlGenerateResultSetQueryByGenerator", env -> + getService(env).sqlGenerateResultSetQueryByGenerator( + getWebSession(env), + getSQLContext(env), + getArgumentVal(env, "generatorId"), getArgumentVal(env, "resultsId"), getResultsRow(env, "selectedRows"), getGeneratorOptions(env) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index a875032932b..1fe80cc480f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -295,31 +295,41 @@ public void run(@NotNull DBRProgressMonitor monitor) throws InvocationTargetExce public String sqlGenerateResultSetQuery( @NotNull WebSession webSession, @NotNull WebSQLContextInfo sqlContext, - @Nullable String generatorId, - @Nullable String generatorKey, + @NotNull String generatorId, + @NotNull String resultsId, + @NotNull List selectedRows, + @NotNull WebSQLGeneratorOptions options + ) throws DBWebException { + checkAndFillTruncatedData(sqlContext, resultsId, selectedRows); + WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows); + return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider), options); + } + + @NotNull + @Override + public String sqlGenerateResultSetQueryByGenerator( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo sqlContext, + @NotNull String generatorId, @NotNull String resultsId, @NotNull List selectedRows, @NotNull WebSQLGeneratorOptions options ) throws DBWebException { - if ((generatorId == null) == (generatorKey == null)) { - throw new DBWebException("Exactly one of generatorId or generatorKey must be specified"); - } if (selectedRows.isEmpty()) { throw new DBWebException("At least one row must be selected"); } - String requestedGeneratorId = generatorKey != null ? generatorKey : generatorId; List applicableGenerators = getApplicableResultSetGenerators( webSession, sqlContext, resultsId, selectedRows ); - if (applicableGenerators.stream().noneMatch(generator -> generator.getId().equals(requestedGeneratorId))) { - throw new DBWebException("Generator '" + requestedGeneratorId + "' is not applicable to this result set"); + 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, requestedGeneratorId, Collections.singletonList(dataProvider), options); + return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider), options); } @NotNull diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java index 190b8466f96..4601cbb6af2 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java @@ -52,15 +52,15 @@ public class GenerateSQLResultSetTest extends CloudbeaverDBTest { } """; - private static final String GQL_GENERATE_QUERY_FROM_RESULTSET_BY_KEY = """ - query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorKey: String!, + private static final String GQL_GENERATE_QUERY_BY_GENERATOR = """ + query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: String!, $resultsId: ID!, $selectedRows: [SQLResultRow!]! ) { - sqlGenerateResultSetQuery( + sqlGenerateResultSetQueryByGenerator( projectId: $projectId connectionId: $connectionId contextId: $contextId - generatorKey: $generatorKey + generatorId: $generatorId resultsId: $resultsId selectedRows: $selectedRows ) @@ -84,22 +84,6 @@ public class GenerateSQLResultSetTest extends CloudbeaverDBTest { } """; - private static final String GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS = """ - query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: SQLResultSetGeneratorId, - $generatorKey: String, $resultsId: ID!, $selectedRows: [SQLResultRow!]! - ) { - sqlGenerateResultSetQuery( - projectId: $projectId - connectionId: $connectionId - contextId: $contextId - generatorId: $generatorId - generatorKey: $generatorKey - resultsId: $resultsId - selectedRows: $selectedRows - ) - } - """; - private List> selectedRows; private WebSQLContextInfo sqlProcessorContext; private String resultId; @@ -175,8 +159,8 @@ public void shouldDiscoverApplicableResultSetGeneratorsInOrder() throws Exceptio } @Test - public void shouldGenerateQueryByDiscoveredGeneratorKey() throws Exception { - String query = generateQuery("dataSelectMany", selectedRows, GQL_GENERATE_QUERY_FROM_RESULTSET_BY_KEY, null, "generatorKey"); + public void shouldGenerateQueryByDiscoveredGenerator() throws Exception { + String query = generateQueryByGenerator("dataSelectMany", selectedRows); Assertions.assertEquals(""" SELECT ID, FIELD @@ -185,47 +169,20 @@ public void shouldGenerateQueryByDiscoveredGeneratorKey() throws Exception { } @Test - public void shouldRejectInapplicableGeneratorKey() { + public void shouldRejectInapplicableGenerator() { DBException exception = Assertions.assertThrows( DBException.class, - () -> generateQuery("tableDDL", selectedRows, GQL_GENERATE_QUERY_FROM_RESULTSET_BY_KEY, null, "generatorKey") + () -> generateQueryByGenerator("tableDDL", selectedRows) ); Assertions.assertTrue(exception.getMessage().contains("not applicable to this result set")); } - @Test - public void shouldRejectMissingGeneratorArguments() { - DBException exception = Assertions.assertThrows( - DBException.class, - () -> executeGenerationQuery(GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS, Map.of()) - ); - - Assertions.assertTrue(exception.getMessage().contains("Exactly one of generatorId or generatorKey must be specified")); - } - - @Test - public void shouldRejectConflictingGeneratorArguments() { - DBException exception = Assertions.assertThrows( - DBException.class, - () -> executeGenerationQuery( - GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS, - Map.of("generatorId", "dataSelect", "generatorKey", "dataSelect") - ) - ); - - Assertions.assertTrue(exception.getMessage().contains("Exactly one of generatorId or generatorKey must be specified")); - } - @Test public void shouldRejectEmptySelectedRows() { DBException exception = Assertions.assertThrows( DBException.class, - () -> executeGenerationQuery( - GQL_GENERATE_QUERY_WITH_OPTIONAL_GENERATORS, - Map.of("generatorKey", "dataSelect"), - List.of() - ) + () -> generateQueryByGenerator("dataSelect", List.of()) ); Assertions.assertTrue(exception.getMessage().contains("At least one row must be selected")); @@ -438,20 +395,9 @@ private String generateQuery( @NotNull List> selectedRows, @NotNull String gqlQuery, @Nullable Map variables - ) throws Exception { - return generateQuery(generatorId, selectedRows, gqlQuery, variables, "generatorId"); - } - - @Nullable - private String generateQuery( - @NotNull String generatorId, - @NotNull List> selectedRows, - @NotNull String gqlQuery, - @Nullable Map variables, - @NotNull String generatorArgument ) throws Exception { Map queryVariables = getBaseVariables(); - queryVariables.put(generatorArgument, generatorId); + queryVariables.put("generatorId", generatorId); queryVariables.put("selectedRows", selectedRows); if (variables != null && !variables.isEmpty()) { queryVariables.putAll(variables); @@ -464,6 +410,22 @@ private String generateQuery( .toString(); } + @Nullable + private String generateQueryByGenerator( + @NotNull String generatorId, + @NotNull List> selectedRows + ) throws Exception { + Map queryVariables = getBaseVariables(); + queryVariables.put("generatorId", generatorId); + queryVariables.put("selectedRows", selectedRows); + Map response = client.executeGQLRequest( + GQL_GENERATE_QUERY_BY_GENERATOR, queryVariables, + Map.of(), "sqlGenerateResultSetQueryByGenerator" + ); + return response.get("data") + .toString(); + } + @NotNull private Map getBaseVariables() { Map queryVariables = new HashMap<>(); @@ -474,18 +436,4 @@ private Map getBaseVariables() { return queryVariables; } - private void executeGenerationQuery(@NotNull String query, @NotNull Map generatorVariables) throws Exception { - executeGenerationQuery(query, generatorVariables, selectedRows); - } - - private void executeGenerationQuery( - @NotNull String query, - @NotNull Map generatorVariables, - @NotNull List> rows - ) throws Exception { - Map variables = getBaseVariables(); - variables.put("selectedRows", rows); - variables.putAll(generatorVariables); - client.executeGQLRequest(query, variables, Map.of(), "sqlGenerateResultSetQuery"); - } } From ccded1f5d519b533adbb150bba8889e344468244 Mon Sep 17 00:00:00 2001 From: "daniil.grigorev" Date: Fri, 21 Aug 2026 10:03:51 +0200 Subject: [PATCH 5/6] dbeaver/pro#9236 [CB] backend: linter fix --- .../bundles/io.cloudbeaver.server/schema/service.sql.graphqls | 2 +- .../src/io/cloudbeaver/service/sql/DBWServiceSQL.java | 2 ++ .../src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index edcfa5db1a7..0e184c84965 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -423,7 +423,7 @@ 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. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 44fb63a79e5..1590ba6d5a4 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -99,6 +99,7 @@ WebAsyncTaskInfo asyncGenerateEntityQuery( @NotNull @WebAction + @Deprecated // use sqlGenerateResultSetQueryByGenerator String sqlGenerateResultSetQuery( @NotNull WebSession session, @NotNull WebSQLContextInfo sqlContext, @@ -119,6 +120,7 @@ String sqlGenerateResultSetQueryByGenerator( @NotNull WebSQLGeneratorOptions options ) throws DBWebException; + @NotNull @WebAction WebSQLContextInfo createContext( @NotNull WebSQLProcessor processor, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 1fe80cc480f..dafbd991f9b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -230,6 +230,7 @@ public DBCLogicalOperator[] getSupportedOperations(@NotNull WebSQLContextInfo co return attribute.getValueHandler().getSupportedOperators(attribute); } + @NotNull @Override public SQLGeneratorDescriptor[] getEntityQueryGenerators( @NotNull WebSession session, From ed9271700ebc378ba3ec37168447405e30cf453c Mon Sep 17 00:00:00 2001 From: "daniil.grigorev" Date: Fri, 21 Aug 2026 10:34:47 +0200 Subject: [PATCH 6/6] dbeaver/pro#9236 [CB] backend: route legacy sqlGenerateResultSetQuery field to sqlGenerateResultSetQueryByGenerator, drop duplicate service method --- .../service/sql/DBWServiceSQL.java | 12 ----------- .../service/sql/WebServiceBindingSQL.java | 2 +- .../service/sql/impl/WebServiceSQL.java | 15 ------------- .../sql/GenerateSQLResultSetTest.java | 21 +++++++++++++++++++ 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 1590ba6d5a4..e6be3a11f64 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -97,18 +97,6 @@ WebAsyncTaskInfo asyncGenerateEntityQuery( @NotNull WebSQLGeneratorOptions options ) throws DBWebException; - @NotNull - @WebAction - @Deprecated // use sqlGenerateResultSetQueryByGenerator - String sqlGenerateResultSetQuery( - @NotNull WebSession session, - @NotNull WebSQLContextInfo sqlContext, - @NotNull String generatorId, - @NotNull String resultsId, - @NotNull List selectedRows, - @NotNull WebSQLGeneratorOptions options - ) throws DBWebException; - @NotNull @WebAction String sqlGenerateResultSetQueryByGenerator( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index 00021a04882..cd3ce445e28 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -109,7 +109,7 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { ) ) .dataFetcher("sqlGenerateResultSetQuery", env -> - getService(env).sqlGenerateResultSetQuery( + getService(env).sqlGenerateResultSetQueryByGenerator( getWebSession(env), getSQLContext(env), getArgumentVal(env, "generatorId"), diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index dafbd991f9b..6cb7ffbbc68 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -291,21 +291,6 @@ public void run(@NotNull DBRProgressMonitor monitor) throws InvocationTargetExce return session.createAndRunAsyncTask("Generate SQL", runnable); } - @NotNull - @Override - public String sqlGenerateResultSetQuery( - @NotNull WebSession webSession, - @NotNull WebSQLContextInfo sqlContext, - @NotNull String generatorId, - @NotNull String resultsId, - @NotNull List selectedRows, - @NotNull WebSQLGeneratorOptions options - ) throws DBWebException { - checkAndFillTruncatedData(sqlContext, resultsId, selectedRows); - WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows); - return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider), options); - } - @NotNull @Override public String sqlGenerateResultSetQueryByGenerator( diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java index 4601cbb6af2..68da4cd347d 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java @@ -188,6 +188,27 @@ public void shouldRejectEmptySelectedRows() { 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 resultSet = clientWrapper.readTaskResultSet(taskId); + resultId = resultSet.get("id").toString(); + List> 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(