From a84c182b562c1946978f14535a9f4dcd213fada8 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Tue, 16 Jun 2026 17:33:55 +0200 Subject: [PATCH 1/5] dbeaver/pro#9568 move tests to ce --- .../test/platform/CEServerTestSuite.java | 12 +- .../test/platform/CloudbeaverDBTest.java | 75 ++++ .../platform/admin/AdminCreateUserTest.java | 58 +++ .../admin/AdminLastLoginTimeTest.java | 130 +++++++ .../sql/DataFilterConstraintsTest.java | 160 +++++++++ .../sql/ForeignKeyNavigationEndpointTest.java | 202 +++++++++++ .../sql/GenerateSQLResultSetTest.java | 308 ++++++++++++++++ .../platform/sql/GroupingEndpointTest.java | 185 ++++++++++ .../test/platform/sql/RowIdResultSetTest.java | 146 ++++++++ .../test/platform/util/DBTestConstants.java | 27 ++ .../util/GraphQLTestClientWrapper.java | 187 ++++++++++ .../platform/util/GraphQLTestConstant.java | 339 ++++++++++++++++++ .../test/platform/util/WebDBTestUtils.java | 67 ++++ 13 files changed, 1895 insertions(+), 1 deletion(-) create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CloudbeaverDBTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminCreateUserTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminLastLoginTimeTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/ForeignKeyNavigationEndpointTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/RowIdResultSetTest.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/DBTestConstants.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestClientWrapper.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestConstant.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/WebDBTestUtils.java diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index f9fa848ea3..81879e52a4 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -23,6 +23,9 @@ import io.cloudbeaver.model.rm.lock.RMLockTest; import io.cloudbeaver.model.session.WebSessionProjectTest; import io.cloudbeaver.model.session.WebSessionTest; +import io.cloudbeaver.test.platform.admin.AdminCreateUserTest; +import io.cloudbeaver.test.platform.admin.AdminLastLoginTimeTest; +import io.cloudbeaver.test.platform.sql.*; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.platform.suite.api.SelectClasses; @@ -40,7 +43,14 @@ NoSessionTest.class, WebSessionTest.class, WebSessionProjectTest.class, - WebNavigatorNodeInfoTest.class + WebNavigatorNodeInfoTest.class, + AdminCreateUserTest.class, + AdminLastLoginTimeTest.class, + GenerateSQLResultSetTest.class, + RowIdResultSetTest.class, + GroupingEndpointTest.class, + ForeignKeyNavigationEndpointTest.class, + DataFilterConstraintsTest.class } ) public class CEServerTestSuite { diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CloudbeaverDBTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CloudbeaverDBTest.java new file mode 100644 index 0000000000..f3edcb86fd --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CloudbeaverDBTest.java @@ -0,0 +1,75 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform; + +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.WebSessionGlobalProjectImpl; +import io.cloudbeaver.app.CEAppStarter; +import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.session.BaseWebSession; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.WebAppUtils; +import io.cloudbeaver.test.WebGQLClient; +import io.cloudbeaver.test.platform.util.GraphQLTestClientWrapper; +import io.cloudbeaver.test.platform.util.WebDBTestUtils; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBUtils; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.LoggingProgressMonitor; +import org.junit.jupiter.api.BeforeEach; + +public abstract class CloudbeaverDBTest extends CloudbeaverMockTest { + + protected final WebGQLClient client = CEAppStarter.createClient(); + protected DBPDataSourceContainer databaseContainer; + protected static JDBCSession databaseSession; + protected final DBRProgressMonitor monitor = new LoggingProgressMonitor(); + + protected final GraphQLTestClientWrapper clientWrapper = new GraphQLTestClientWrapper(client); + protected WebSessionGlobalProjectImpl globalProject; + protected WebConnectionInfo webConnectionInfo; + protected WebSession webSession; + + @BeforeEach + public void init() throws Exception { + String sessionId = clientWrapper.openSession(); + webSession = resolveWebSession(sessionId); + globalProject = webSession.getGlobalProject(); + if (globalProject == null) { + throw new DBException("Global project is not configured"); + } + databaseContainer = WebDBTestUtils.createH2DataSource(monitor, globalProject); + globalProject.getDataSourceRegistry().addDataSource(databaseContainer); + databaseSession = DBUtils.openUtilSession(monitor, databaseContainer, "Internal database session"); + databaseSession.enableLogging(false); + webConnectionInfo = globalProject.addConnection(databaseContainer); + } + + private static WebSession resolveWebSession(String sessionId) throws DBException { + var sessionManager = WebAppUtils.getWebApplication().getSessionManager(); + BaseWebSession session = sessionId == null ? null : sessionManager.getSession(sessionId); + if (session instanceof WebSession ws) { + return ws; + } + return (WebSession) sessionManager.getAllActiveSessions().stream() + .findFirst() + .orElseThrow(() -> new DBException("No active web session")); + } + +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminCreateUserTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminCreateUserTest.java new file mode 100644 index 0000000000..95bc310421 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminCreateUserTest.java @@ -0,0 +1,58 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.admin; + +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; +import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +public class AdminCreateUserTest extends CloudbeaverMockTest { + + private static final String GQL_CREATE_USER = """ + query createUser($userId: ID!, $enabled: Boolean!, $authRole: String) { + result: createUser(userId: $userId, enabled: $enabled, authRole: $authRole) { userId } + }"""; + + private static final String GQL_DELETE_USER = """ + query deleteUser($userId: ID!) { + result: deleteUser(userId: $userId) + }"""; + + @Test + public void testCreateUserTrimsUserName() throws Exception { + WebGQLClient adminClient = CEAppStarter.createClient(); + CEAppStarter.authenticateTestUser(adminClient); + + String expectedUserId = "trim-user-test"; + String paddedUserName = " " + expectedUserId + " "; + try { + Map created = adminClient.sendQuery( + GQL_CREATE_USER, + Map.of("userId", paddedUserName, "enabled", true, "authRole", "user") + ); + Assertions.assertNotNull(created); + Assertions.assertEquals(expectedUserId, JSONUtils.getString(created, "userId")); + } finally { + adminClient.sendQuery(GQL_DELETE_USER, Map.of("userId", expectedUserId)); + } + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminLastLoginTimeTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminLastLoginTimeTest.java new file mode 100644 index 0000000000..b5fe82ce9d --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/admin/AdminLastLoginTimeTest.java @@ -0,0 +1,130 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.admin; + +import io.cloudbeaver.CloudbeaverMockTest; +import io.cloudbeaver.app.CEAppStarter; +import io.cloudbeaver.auth.provider.local.LocalAuthProvider; +import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.SecurityUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +public class AdminLastLoginTimeTest extends CloudbeaverMockTest { + + private static final String TEST_USER_ID = "last-login-test-user"; + private static final String TEST_USER_PASSWORD = SecurityUtils.makeDigest("12345"); + + private static final String GQL_CREATE_USER = """ + query createUser($userId: ID!, $enabled: Boolean!, $authRole: String) { + result: createUser(userId: $userId, enabled: $enabled, authRole: $authRole) { userId } + }"""; + + private static final String GQL_DELETE_USER = """ + query deleteUser($userId: ID!) { + result: deleteUser(userId: $userId) + }"""; + + private static final String GQL_SET_USER_CREDENTIALS = """ + query setUserCredentials($userId: ID!, $providerId: ID!, $credentials: Object!) { + result: setUserCredentials(userId: $userId, providerId: $providerId, credentials: $credentials) + }"""; + + private static final String GQL_ADMIN_USER_INFO = """ + query adminUserInfo($userId: ID!) { + result: adminUserInfo(userId: $userId) { + userId + lastLoginTime + } + }"""; + + @Test + public void testLastLoginTimePopulatedAfterAuthentication() throws Exception { + + // GIVEN + WebGQLClient adminClient = CEAppStarter.createClient(); + CEAppStarter.authenticateTestUser(adminClient); + + try { + Map created = adminClient.sendQuery( + GQL_CREATE_USER, + Map.of("userId", TEST_USER_ID, "enabled", true, "authRole", "user") + ); + Assertions.assertNotNull(created); + Assertions.assertEquals(TEST_USER_ID, JSONUtils.getString(created, "userId")); + + adminClient.sendQuery( + GQL_SET_USER_CREDENTIALS, + Map.of( + "userId", TEST_USER_ID, + "providerId", LocalAuthProvider.PROVIDER_ID, + "credentials", Map.of("password", TEST_USER_PASSWORD) + ) + ); + + Map beforeLogin = adminClient.sendQuery( + GQL_ADMIN_USER_INFO, + Map.of("userId", TEST_USER_ID) + ); + Assertions.assertNotNull(beforeLogin); + Assertions.assertEquals(TEST_USER_ID, JSONUtils.getString(beforeLogin, "userId")); + Assertions.assertNull( + JSONUtils.getString(beforeLogin, "lastLoginTime"), + "lastLoginTime should be null before the first successful authentication" + ); + + WebGQLClient userClient = CEAppStarter.createClient(); + Map authInfo = userClient.sendQuery( + WebGQLClient.GQL_AUTHENTICATE, + Map.of( + "provider", LocalAuthProvider.PROVIDER_ID, + "credentials", Map.of( + LocalAuthProvider.CRED_USER, TEST_USER_ID, + LocalAuthProvider.CRED_PASSWORD, TEST_USER_PASSWORD + ) + ) + ); + Assertions.assertNotNull(authInfo); + Assertions.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + + // WHEN + Map afterLogin = adminClient.sendQuery( + GQL_ADMIN_USER_INFO, + Map.of("userId", TEST_USER_ID) + ); + + // THEN + Assertions.assertNotNull(afterLogin); + Assertions.assertEquals(TEST_USER_ID, JSONUtils.getString(afterLogin, "userId")); + String lastLoginTime = JSONUtils.getString(afterLogin, "lastLoginTime"); + Assertions.assertNotNull( + lastLoginTime, + "lastLoginTime must be returned for an authenticated user" + ); + Assertions.assertFalse( + lastLoginTime.isBlank(), + "lastLoginTime must not be empty" + ); + } finally { + adminClient.sendQuery(GQL_DELETE_USER, Map.of("userId", TEST_USER_ID)); + } + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java new file mode 100644 index 0000000000..43c97032b2 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java @@ -0,0 +1,160 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.sql; + +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebServiceBindingSQL; +import io.cloudbeaver.test.platform.CloudbeaverDBTest; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class DataFilterConstraintsTest extends CloudbeaverDBTest { + + private static WebSQLContextInfo sqlProcessorContext; + + @BeforeEach + public void prepareTables() throws Exception { + try (JDBCStatement stmt = databaseSession.createStatement()) { + Assertions.assertFalse(stmt.execute("CREATE TABLE TEST_TABLE (id IDENTITY NOT NULL PRIMARY KEY, text_column VARCHAR)")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (text_column) VALUES ('value_1')")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (text_column) VALUES ('value_2')")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (text_column) VALUES ('value_3')")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (text_column) VALUES (null)")); + } + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + sqlProcessorContext = sqlProcessor.createContext( + null, "PUBLIC", globalProject.getId() + ); + } + + @Test + public void shouldApplyEqualsDataFilter() throws Exception { + // Given + Map textConstraint = new HashMap<>(); + textConstraint.put("attributeName", "TEXT_COLUMN"); + textConstraint.put("operator", "EQUALS"); + textConstraint.put("value", "value_2"); + Map dataFilter = Map.of( + "limit", 200, + "offset", 0, + "constraints", List.of(textConstraint) + ); + String taskId = clientWrapper.asyncReadDataFromContainer( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + resolveNodePath(), + dataFilter + ); + clientWrapper.waitTaskCompleted(taskId); + + // When + Map response = clientWrapper.readTaskResultSet(taskId); + + // Then + List> rows = JSONUtils.getObjectList(response, "rowsWithMetaData"); + Assertions.assertEquals(1, rows.size()); + String responseJson = JSONUtils.GSON.toJson(response); + Assertions.assertFalse(responseJson.contains("value_1")); + Assertions.assertTrue(responseJson.contains("value_2")); + Assertions.assertFalse(responseJson.contains("value_3")); + } + + @Test + public void shouldApplyNonEqualsDataFilter() throws Exception { + // Given + Map textConstraint = new HashMap<>(); + textConstraint.put("attributeName", "TEXT_COLUMN"); + textConstraint.put("operator", "NOT_EQUALS"); + textConstraint.put("value", "value_3"); + Map dataFilter = Map.of( + "limit", 200, + "offset", 0, + "constraints", List.of(textConstraint) + ); + String taskId = clientWrapper.asyncReadDataFromContainer( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + resolveNodePath(), + dataFilter + ); + clientWrapper.waitTaskCompleted(taskId); + + // When + Map response = clientWrapper.readTaskResultSet(taskId); + + // Then + List> rows = JSONUtils.getObjectList(response, "rowsWithMetaData"); + Assertions.assertEquals(2, rows.size()); + String responseJson = JSONUtils.GSON.toJson(response); + Assertions.assertTrue(responseJson.contains("value_1")); + Assertions.assertTrue(responseJson.contains("value_2")); + Assertions.assertFalse(responseJson.contains("value_3")); + } + + @Test + public void shouldApplyIsNullsDataFilter() throws Exception { + // Given + Map textConstraint = new HashMap<>(); + textConstraint.put("attributeName", "TEXT_COLUMN"); + textConstraint.put("operator", "IS_NULL"); + Map dataFilter = Map.of( + "limit", 200, + "offset", 0, + "constraints", List.of(textConstraint) + ); + String taskId = clientWrapper.asyncReadDataFromContainer( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + resolveNodePath(), + dataFilter + ); + clientWrapper.waitTaskCompleted(taskId); + + // When + Map response = clientWrapper.readTaskResultSet(taskId); + + // Then + List> rows = JSONUtils.getObjectList(response, "rowsWithMetaData"); + Assertions.assertEquals(1, rows.size()); + String responseJson = JSONUtils.GSON.toJson(response); + Assertions.assertFalse(responseJson.contains("value_1")); + Assertions.assertFalse(responseJson.contains("value_2")); + Assertions.assertFalse(responseJson.contains("value_3")); + } + + @NotNull + private String resolveNodePath() { + return String.format( + "database://%s/PUBLIC/org.jkiss.dbeaver.ext.h2.model.H2Table/TEST_TABLE", + databaseContainer.getId() + ); + } + +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/ForeignKeyNavigationEndpointTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/ForeignKeyNavigationEndpointTest.java new file mode 100644 index 0000000000..1c7d65dd95 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/ForeignKeyNavigationEndpointTest.java @@ -0,0 +1,202 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.sql; + +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebServiceBindingSQL; +import io.cloudbeaver.test.platform.CloudbeaverDBTest; +import io.cloudbeaver.test.platform.util.GraphQLTestConstant; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ForeignKeyNavigationEndpointTest extends CloudbeaverDBTest { + + @BeforeEach + public void prepareTables() throws SQLException { + try (JDBCStatement stmt = databaseSession.createStatement()) { + Assertions.assertFalse(stmt.execute("DROP TABLE IF EXISTS FK_NAV_ORDER")); + Assertions.assertFalse(stmt.execute("DROP TABLE IF EXISTS FK_NAV_CUSTOMER")); + Assertions.assertFalse(stmt.execute("CREATE TABLE FK_NAV_CUSTOMER (ID INT PRIMARY KEY, NAME VARCHAR(128))")); + Assertions.assertFalse(stmt.execute(""" + CREATE TABLE FK_NAV_ORDER ( + ID INT PRIMARY KEY, + CUSTOMER_ID INT NOT NULL, + CONSTRAINT FK_NAV_ORDER_CUSTOMER FOREIGN KEY (CUSTOMER_ID) REFERENCES FK_NAV_CUSTOMER(ID) + ) + """)); + Assertions.assertFalse(stmt.execute("INSERT INTO FK_NAV_CUSTOMER (ID, NAME) VALUES (1, 'Alice')")); + Assertions.assertFalse(stmt.execute("INSERT INTO FK_NAV_CUSTOMER (ID, NAME) VALUES (2, 'Bob')")); + Assertions.assertFalse(stmt.execute("INSERT INTO FK_NAV_ORDER (ID, CUSTOMER_ID) VALUES (10, 1)")); + Assertions.assertFalse(stmt.execute("INSERT INTO FK_NAV_ORDER (ID, CUSTOMER_ID) VALUES (11, 2)")); + } + } + + @Test + public void shouldReadReferencedRowByForeignKeyCell() throws Exception { + // Given: an order result set exposing a forward FK from CUSTOMER_ID to FK_NAV_CUSTOMER. + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId()); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT ID, CUSTOMER_ID FROM FK_NAV_ORDER ORDER BY ID" + ); + clientWrapper.waitTaskCompleted(taskId); + + Map orderResultSet = clientWrapper.readTaskResultSet(taskId); + List> associations = JSONUtils.getObjectList(orderResultSet, "associations"); + Assertions.assertFalse(associations.isEmpty(), "Forward FK is missing on FK_NAV_ORDER result set"); + Map forwardRef = associations.getFirst(); + Assertions.assertEquals("FK_NAV_ORDER_CUSTOMER", JSONUtils.getString(forwardRef, "associationName")); + Assertions.assertTrue( + JSONUtils.getString(forwardRef, "targetEntityName").contains("FK_NAV_CUSTOMER"), + "Forward target should be FK_NAV_CUSTOMER" + ); + + List> orderRows = JSONUtils.getObjectList(orderResultSet, "rowsWithMetaData"); + Assertions.assertEquals(2, orderRows.size()); + Map orderRow = orderRows.getFirst(); + + // When: navigating from the FK cell value to the referenced customer row. + int columnIndex = asIntegerList(forwardRef.get("columnIndexList")).getFirst(); + Map navigateVars = new HashMap<>(); + navigateVars.put("projectId", globalProject.getId()); + navigateVars.put("connectionId", databaseContainer.getId()); + navigateVars.put("contextId", sqlProcessorContext.getId()); + navigateVars.put("resultsId", String.valueOf(orderResultSet.get("id"))); + navigateVars.put("row", orderRow); + navigateVars.put("columnIndex", columnIndex); + navigateVars.put("associationName", JSONUtils.getString(forwardRef, "associationName")); + navigateVars.put("isReference", false); + navigateVars.put("dataFormat", null); + + Map navigateResponse = client.sendQuery(GraphQLTestConstant.GQL_ASYNC_SQL_NAVIGATE_FOREIGN_KEY, navigateVars); + Assertions.assertNotNull(navigateResponse); + String navigateTaskId = JSONUtils.getString(navigateResponse, "id"); + Assertions.assertNotNull(navigateTaskId); + clientWrapper.waitTaskCompleted(navigateTaskId); + + // Then: the navigation result contains only the referenced customer. + Map customerResultSet = clientWrapper.readTaskResultSet(navigateTaskId); + List> customerRows = JSONUtils.getObjectList(customerResultSet, "rowsWithMetaData"); + Assertions.assertEquals(1, customerRows.size()); + List customerData = (List) customerRows.getFirst().get("data"); + Assertions.assertEquals("1", String.valueOf(customerData.get(0))); + Assertions.assertEquals("Alice", customerData.get(1)); + } + + @Test + public void shouldExposeReverseReferenceOnReferencedColumn() throws Exception { + // Given: a customer result set; FK_NAV_ORDER.CUSTOMER_ID references FK_NAV_CUSTOMER.ID. + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId()); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT ID, NAME FROM FK_NAV_CUSTOMER ORDER BY ID" + ); + clientWrapper.waitTaskCompleted(taskId); + + Map customerResultSet = clientWrapper.readTaskResultSet(taskId); + + // Then: result set carries a reverse reference describing the FK from FK_NAV_ORDER, + // attached to the PK column (ID at index 0) via columnIndexList. + List> references = JSONUtils.getObjectList(customerResultSet, "references"); + Assertions.assertFalse(references.isEmpty(), "Reverse reference is missing on FK_NAV_CUSTOMER result set"); + Map reverseRef = references.getFirst(); + Assertions.assertEquals("FK_NAV_ORDER_CUSTOMER", JSONUtils.getString(reverseRef, "associationName")); + String reverseTarget = JSONUtils.getString(reverseRef, "targetEntityName"); + Assertions.assertNotNull(reverseTarget); + Assertions.assertTrue( + reverseTarget.contains("FK_NAV_ORDER"), + "Reverse reference target should point to the referencing entity FK_NAV_ORDER, got: " + reverseTarget + ); + // Reverse ref targets the PK column ID at index 0 + Assertions.assertEquals(List.of(0), asIntegerList(reverseRef.get("columnIndexList"))); + + Assertions.assertTrue( + JSONUtils.getObjectList(customerResultSet, "associations").isEmpty(), + "FK_NAV_CUSTOMER result set should not expose any forward associations" + ); + } + + @Test + public void shouldNavigateReverseReferenceFromParentRow() throws Exception { + // Given: a customer result set positioned on Alice (ID=1). + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId()); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT ID, NAME FROM FK_NAV_CUSTOMER ORDER BY ID" + ); + clientWrapper.waitTaskCompleted(taskId); + + Map customerResultSet = clientWrapper.readTaskResultSet(taskId); + List> references = JSONUtils.getObjectList(customerResultSet, "references"); + Assertions.assertFalse(references.isEmpty(), "Reverse reference is missing"); + Map reverseRef = references.getFirst(); + + List> customerRows = JSONUtils.getObjectList(customerResultSet, "rowsWithMetaData"); + Assertions.assertEquals(2, customerRows.size()); + Map aliceRow = customerRows.getFirst(); + + // When: navigating using any one of columnIndexList to identify the source entity. + int columnIndex = asIntegerList(reverseRef.get("columnIndexList")).getFirst(); + Map navigateVars = new HashMap<>(); + navigateVars.put("projectId", globalProject.getId()); + navigateVars.put("connectionId", databaseContainer.getId()); + navigateVars.put("contextId", sqlProcessorContext.getId()); + navigateVars.put("resultsId", String.valueOf(customerResultSet.get("id"))); + navigateVars.put("row", aliceRow); + navigateVars.put("columnIndex", columnIndex); + navigateVars.put("associationName", JSONUtils.getString(reverseRef, "associationName")); + navigateVars.put("isReference", true); + navigateVars.put("dataFormat", null); + + Map navigateResponse = client.sendQuery(GraphQLTestConstant.GQL_ASYNC_SQL_NAVIGATE_FOREIGN_KEY, navigateVars); + Assertions.assertNotNull(navigateResponse); + String navigateTaskId = JSONUtils.getString(navigateResponse, "id"); + Assertions.assertNotNull(navigateTaskId); + clientWrapper.waitTaskCompleted(navigateTaskId); + + // Then: navigation returns Alice's single order (ID=10, CUSTOMER_ID=1). + Map orderResultSet = clientWrapper.readTaskResultSet(navigateTaskId); + List> orderRows = JSONUtils.getObjectList(orderResultSet, "rowsWithMetaData"); + Assertions.assertEquals(1, orderRows.size()); + List orderData = (List) orderRows.getFirst().get("data"); + Assertions.assertEquals("10", String.valueOf(orderData.get(0))); + Assertions.assertEquals("1", String.valueOf(orderData.get(1))); + } + + private static List asIntegerList(Object value) { + List raw = (List) value; + return raw.stream().map(n -> ((Number) n).intValue()).toList(); + } +} 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 new file mode 100644 index 0000000000..fc29336c4c --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GenerateSQLResultSetTest.java @@ -0,0 +1,308 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.sql; + +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebServiceBindingSQL; +import io.cloudbeaver.test.platform.CloudbeaverDBTest; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +public class GenerateSQLResultSetTest extends CloudbeaverDBTest { + + private static final String GQL_GENERATE_QUERY_FROM_RESULTSET = """ + query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: SQLResultSetGeneratorId!, + $resultsId: ID!, $selectedRows: [SQLResultRow!]! + ) { + sqlGenerateResultSetQuery( + projectId: $projectId + connectionId: $connectionId + contextId: $contextId + generatorId: $generatorId + resultsId: $resultsId + selectedRows: $selectedRows + ) + } + """; + + private List> selectedRows; + private WebSQLContextInfo sqlProcessorContext; + private String resultId; + + @BeforeEach + public void prepareTables() throws Exception { + try (JDBCStatement stmt = databaseSession.createStatement()) { + Assertions.assertFalse(stmt.execute("CREATE TABLE TEST_TABLE (id IDENTITY NOT NULL PRIMARY KEY, field VARCHAR)")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (field) VALUES ('value_1')")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (field) VALUES ('value_2')")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (field) VALUES ('value_3')")); + Assertions.assertFalse(stmt.execute("INSERT INTO TEST_TABLE (field) VALUES ('value_4')")); + } + + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + sqlProcessorContext = sqlProcessor.createContext( + null, "PUBLIC", globalProject.getId() + ); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT * FROM TEST_TABLE ORDER BY id LIMIT 3" + ); + clientWrapper.waitTaskCompleted(taskId); + + Map resultSet = clientWrapper.readTaskResultSet(taskId); + resultId = resultSet.get("id").toString(); + List> rows = JSONUtils.getObjectList(resultSet, "rowsWithMetaData"); + Assertions.assertNotNull(rows); + Assertions.assertEquals(3, rows.size()); + selectedRows = rows.subList(0, 2); + } + + @Test + public void shouldGenerateSelectQueryFromResultSet() throws Exception { + // When + String query = generateQuery("dataSelect", selectedRows); + + // Then + Assertions.assertEquals(""" + SELECT ID, FIELD + FROM PUBLIC.TEST_TABLE + WHERE ID=1; + SELECT ID, FIELD + FROM PUBLIC.TEST_TABLE + WHERE ID=2;""", query + ); + } + + @Test + public void shouldGenerateSelectQueryWithoutFullyQualifiedNames() throws Exception { + // When + String gqlQuery = """ + query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: SQLResultSetGeneratorId!, + $resultsId: ID!, $selectedRows: [SQLResultRow!]!, $generatorOptions: SQLQueryGeneratorOptions + ) { + sqlGenerateResultSetQuery( + projectId: $projectId + connectionId: $connectionId + contextId: $contextId + generatorId: $generatorId + resultsId: $resultsId + selectedRows: $selectedRows + generatorOptions: $generatorOptions + ) + }"""; + Map variables = Map.of( + "generatorOptions", + Map.of( + "useFullyQualifiedNames", false, + "compactSql", false + ) + ); + String query = generateQuery("dataSelect", selectedRows, gqlQuery, variables); + + // Then + Assertions.assertEquals(""" + SELECT ID, FIELD + FROM TEST_TABLE + WHERE ID=1; + SELECT ID, FIELD + FROM TEST_TABLE + WHERE ID=2;""", query + ); + } + + @Test + public void shouldGenerateSelectQueryWithCompactSQL() throws Exception { + // When + String gqlQuery = """ + query($projectId: ID, $connectionId: ID!, $contextId: ID!, $generatorId: SQLResultSetGeneratorId!, + $resultsId: ID!, $selectedRows: [SQLResultRow!]!, $generatorOptions: SQLQueryGeneratorOptions + ) { + sqlGenerateResultSetQuery( + projectId: $projectId + connectionId: $connectionId + contextId: $contextId + generatorId: $generatorId + resultsId: $resultsId + selectedRows: $selectedRows + generatorOptions: $generatorOptions + ) + }"""; + Map variables = Map.of( + "generatorOptions", + Map.of( + "useFullyQualifiedNames", true, + "compactSql", true + ) + ); + String query = generateQuery("dataSelect", selectedRows, gqlQuery, variables); + + // Then + Assertions.assertEquals(""" + SELECT ID, FIELD FROM PUBLIC.TEST_TABLE WHERE ID=1; + SELECT ID, FIELD FROM PUBLIC.TEST_TABLE WHERE ID=2;""", query + ); + } + + @Test + public void shouldGenerateSelectInQueryFromResultSet() throws Exception { + // When + String query = generateQuery("dataSelectMany", selectedRows); + + // Then + Assertions.assertEquals(""" + SELECT ID, FIELD + FROM PUBLIC.TEST_TABLE + WHERE ID IN (1,2);""", query + ); + } + + @Test + public void shouldGenerateInsertQueryFromResultSet() throws Exception { + // When + String query = generateQuery("dataInsert", selectedRows); + + // Then + Assertions.assertEquals(""" + INSERT INTO PUBLIC.TEST_TABLE + (ID, FIELD) + VALUES(1, 'value_1'); + INSERT INTO PUBLIC.TEST_TABLE + (ID, FIELD) + VALUES(2, 'value_2');""", query + ); + } + + @Test + public void shouldGenerateUpdateQueryFromResultSet() throws Exception { + // When + String query = generateQuery("dataUpdate", selectedRows); + + // Then + Assertions.assertEquals(""" + UPDATE PUBLIC.TEST_TABLE + SET FIELD='value_1' + WHERE ID=1; + UPDATE PUBLIC.TEST_TABLE + SET FIELD='value_2' + WHERE ID=2;""", query + ); + } + + @Test + public void shouldGenerateDeleteQueryFromResultSet() throws Exception { + // When + String query = generateQuery("dataDeleteByUniqueKey", selectedRows); + + // Then + Assertions.assertEquals(""" + DELETE FROM PUBLIC.TEST_TABLE + WHERE ID=1; + DELETE FROM PUBLIC.TEST_TABLE + WHERE ID=2;""", query + ); + } + + @Test + public void shouldGenerateInsertQueryWithLongtext() throws Exception { + // Given + String endOfFile = "End of file"; + // Values over 4096 symbols long are truncated + String longtext = "longTextDataSample".repeat(300) + endOfFile; + try (JDBCStatement stmt = databaseSession.createStatement()) { + Assertions.assertFalse(stmt.execute("CREATE TABLE LONGTEXT_TEST_TABLE (id IDENTITY NOT NULL PRIMARY KEY, longtext VARCHAR)")); + Assertions.assertFalse(stmt.execute("INSERT INTO LONGTEXT_TEST_TABLE (longtext) VALUES ('" + longtext + "')")); + Assertions.assertFalse(stmt.execute("INSERT INTO LONGTEXT_TEST_TABLE (longtext) VALUES ('')")); + } + String longtextTaskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT * FROM LONGTEXT_TEST_TABLE ORDER BY ID LIMIT 1" + ); + clientWrapper.waitTaskCompleted(longtextTaskId); + Map resultSet = clientWrapper.readTaskResultSet(longtextTaskId); + resultId = resultSet.get("id").toString(); + List> rows = JSONUtils.getObjectList(resultSet, "rowsWithMetaData"); + Assertions.assertNotNull(rows); + Assertions.assertEquals(1, rows.size()); + ArrayList data = (ArrayList) rows.getFirst().get("data"); + String truncatedLongtext = ((Map) data.get(1)).get("text"); + Assertions.assertFalse(truncatedLongtext.endsWith(endOfFile)); + // Remove 'type' and 'contentLength' data attributes which is done on front-end + data.remove(1); + data.add(truncatedLongtext); + + // When + String query = generateQuery("dataInsert", rows); + + // Then + String expectedQuery = String.format(""" + INSERT INTO PUBLIC.LONGTEXT_TEST_TABLE + (ID, LONGTEXT) + VALUES(1, '%s');""", + longtext + ); + Assertions.assertEquals(expectedQuery, query); + } + + @Nullable + private String generateQuery( + @NotNull String generatorId, + @NotNull List> selectedRows + ) throws Exception { + return generateQuery(generatorId, selectedRows, GQL_GENERATE_QUERY_FROM_RESULTSET, null); + } + + @Nullable + private String generateQuery( + @NotNull String generatorId, + @NotNull List> selectedRows, + @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); + queryVariables.put("selectedRows", selectedRows); + if (variables != null && !variables.isEmpty()) { + queryVariables.putAll(variables); + } + Map response = client.executeGQLRequest( + gqlQuery, queryVariables, + Map.of(), "sqlGenerateResultSetQuery" + ); + return response.get("data") + .toString(); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java new file mode 100644 index 0000000000..15f108188b --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java @@ -0,0 +1,185 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.sql; + +import io.cloudbeaver.WebSessionGlobalProjectImpl; +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebServiceBindingSQL; +import io.cloudbeaver.test.platform.CloudbeaverDBTest; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.cloudbeaver.test.platform.util.GraphQLTestConstant.GQL_SQL_GROUPING_RESULTSET; + + +public class GroupingEndpointTest extends CloudbeaverDBTest { + + @BeforeEach + public void prepareTables() throws SQLException { + try (JDBCStatement stmt = databaseSession.createStatement()) { + + Assertions.assertFalse(stmt.execute( + "CREATE TABLE GROUP_DATA (id IDENTITY PRIMARY KEY, C VARCHAR(128), USER_NAME VARCHAR(128))")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupA','alice')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupA','bob')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupA','alice')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupA','carol')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupB','dave')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupB','ellen')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupB','dave')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupC','frank')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupC','george')")); + Assertions.assertFalse(stmt.execute( + "INSERT INTO GROUP_DATA (C, USER_NAME) VALUES ('groupC','frank')")); + } + } + + @Test + public void sqlGrouping_whenGroupingByOneColumn_thenReturn() throws Exception { + + //GIVEN + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId()); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT C, USER_NAME FROM GROUP_DATA" + ); + clientWrapper.waitTaskCompleted(taskId); + String resultId = clientWrapper.readTaskResultId(taskId); + + //WHEN + Map groupResponse = callGroupingSql(globalProject, sqlProcessorContext, resultId, List.of("C")); + + //THEN + Assertions.assertNotNull(groupResponse); + String sqlGroupingTaskId = (String) groupResponse.get("id"); + Assertions.assertNotNull(sqlGroupingTaskId); + clientWrapper.waitTaskCompleted(sqlGroupingTaskId); + clientWrapper.readTaskResultId( + sqlGroupingTaskId, rowsWithMetadata -> { + Assertions.assertNotNull(rowsWithMetadata); + Assertions.assertFalse(rowsWithMetadata.isEmpty()); + Assertions.assertEquals(3, rowsWithMetadata.size()); + for (Map row : rowsWithMetadata) { + List data = JSONUtils.getStringList(row, "data"); + String groupName = data.get(0); + int count = Integer.parseInt(data.get(1)); + switch (groupName) { + case "groupA" -> Assertions.assertEquals(4, count); + case "groupB" -> Assertions.assertEquals(3, count); + case "groupC" -> Assertions.assertEquals(3, count); + default -> throw new IllegalStateException("Unexpected group name: " + groupName); + } + } + } + ); + } + + @Test + public void sqlGrouping_whenGroupingByTwoColumns_thenReturn() throws Exception { + + // GIVEN + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext(null, "PUBLIC", globalProject.getId()); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT C, USER_NAME FROM GROUP_DATA" + ); + clientWrapper.waitTaskCompleted(taskId); + String resultId = clientWrapper.readTaskResultId(taskId); + + // WHEN + Map groupResponse = callGroupingSql(globalProject, sqlProcessorContext, resultId, List.of("C", "USER_NAME")); + + // THEN + Assertions.assertNotNull(groupResponse); + String sqlGroupingTaskId = (String) groupResponse.get("id"); + Assertions.assertNotNull(sqlGroupingTaskId); + clientWrapper.waitTaskCompleted(sqlGroupingTaskId); + clientWrapper.readTaskResultId( + sqlGroupingTaskId, rowsWithMetadata -> { + Assertions.assertNotNull(rowsWithMetadata); + Assertions.assertFalse(rowsWithMetadata.isEmpty()); + Assertions.assertEquals(7, rowsWithMetadata.size()); + for (Map row : rowsWithMetadata) { + List data = JSONUtils.getStringList(row, "data"); + Assertions.assertEquals(3, data.size(), "Unexpected grouping row size"); + String groupName = data.get(0); + String userName = data.get(1); + int count = Integer.parseInt(data.get(2)); + String key = groupName + ":" + userName; + switch (key) { + case "groupA:alice" -> Assertions.assertEquals(2, count); + case "groupA:bob" -> Assertions.assertEquals(1, count); + case "groupA:carol" -> Assertions.assertEquals(1, count); + case "groupB:dave" -> Assertions.assertEquals(2, count); + case "groupB:ellen" -> Assertions.assertEquals(1, count); + case "groupC:frank" -> Assertions.assertEquals(2, count); + case "groupC:george" -> Assertions.assertEquals(1, count); + default -> throw new IllegalStateException("Unexpected group key: " + key); + } + } + } + ); + } + + @NotNull + private Map callGroupingSql( + @NotNull WebSessionGlobalProjectImpl globalProject, + @NotNull WebSQLContextInfo sqlProcessorContext, + @NotNull String resultId, + @NotNull List columnNames + ) throws Exception { + Map groupVars = new HashMap<>(); + groupVars.put("projectId", globalProject.getId()); + groupVars.put("contextId", sqlProcessorContext.getId()); + groupVars.put("connectionId", databaseContainer.getId()); + groupVars.put("resultsId", resultId); + groupVars.put("columnNames", columnNames); + groupVars.put("functions", "COUNT(*)"); + groupVars.put("showDuplicatesOnly", false); + groupVars.put("filter", null); + groupVars.put("dataFormat", null); + groupVars.put("isInteractive", false); + + return client.sendQuery(GQL_SQL_GROUPING_RESULTSET, groupVars); + } + +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/RowIdResultSetTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/RowIdResultSetTest.java new file mode 100644 index 0000000000..07b2df9761 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/RowIdResultSetTest.java @@ -0,0 +1,146 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.sql; + +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.service.sql.WebSQLProcessor; +import io.cloudbeaver.service.sql.WebSQLResultSetRowIdentifier; +import io.cloudbeaver.service.sql.WebServiceBindingSQL; +import io.cloudbeaver.test.platform.CloudbeaverDBTest; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + + +public class RowIdResultSetTest extends CloudbeaverDBTest { + + @BeforeEach + public void prepareTables() throws SQLException { + try (JDBCStatement stmt = databaseSession.createStatement()) { + Assertions.assertFalse(stmt.execute("CREATE TABLE PK (id IDENTITY NOT NULL PRIMARY KEY, a VARCHAR)")); + Assertions.assertFalse(stmt.execute("INSERT INTO PK (a) VALUES ('test_1')")); + + Assertions.assertFalse(stmt.execute("CREATE TABLE NO_PK (a INT, b VARCHAR)")); + Assertions.assertFalse(stmt.execute("INSERT INTO NO_PK (a, b) VALUES (1, 'test_1')")); + + Assertions.assertFalse(stmt.execute("CREATE TABLE COMPOSITE_PK (a INT, b VARCHAR, PRIMARY KEY (a,b))")); + Assertions.assertFalse(stmt.execute("INSERT INTO COMPOSITE_PK (a, b) VALUES (1, 'test_1')")); + } + } + + @Test + public void shouldReturnRowIdentifierDetailsWithPK() throws Exception { + // Given + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext( + null, "PUBLIC", globalProject.getId() + ); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT * FROM PK" + ); + clientWrapper.waitTaskCompleted(taskId); + + // When + Map response = clientWrapper.readTaskResultSet(taskId); + + // Then + String rowIdentifierState = String.valueOf(response.get("rowIdentifierState")); + Assertions.assertEquals( + WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState.PRIMARY_KEY.name(), + rowIdentifierState + ); + Map rowIdentifier = (Map) response.get("rowIdentifier"); + Assertions.assertEquals("PRIMARY KEY", rowIdentifier.get("constraintType")); + List> attributes = JSONUtils.getObjectList(rowIdentifier, "attributes"); + Assertions.assertEquals(1, attributes.size()); + Map attribute = attributes.getFirst(); + Assertions.assertEquals("ID", attribute.get("name")); + Assertions.assertEquals(0.0, attribute.get("ordinalPosition")); + } + + @Test + public void shouldReturnRowIdentifierDetailsWithCompositePK() throws Exception { + // Given + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext( + null, "PUBLIC", globalProject.getId() + ); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT * FROM COMPOSITE_PK" + ); + clientWrapper.waitTaskCompleted(taskId); + + // When + Map response = clientWrapper.readTaskResultSet(taskId); + + // Then + String rowIdentifierState = String.valueOf(response.get("rowIdentifierState")); + Assertions.assertEquals( + WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState.PRIMARY_KEY.name(), + rowIdentifierState + ); + Map rowIdentifier = (Map) response.get("rowIdentifier"); + Assertions.assertEquals("PRIMARY KEY", rowIdentifier.get("constraintType")); + List> attributes = JSONUtils.getObjectList(rowIdentifier, "attributes"); + Assertions.assertEquals(2, attributes.size()); + Map first = attributes.getFirst(); + Assertions.assertEquals("A", first.get("name")); + Assertions.assertEquals(0.0, first.get("ordinalPosition")); + Map second = attributes.get(1); + Assertions.assertEquals("B", second.get("name")); + Assertions.assertEquals(1.0, second.get("ordinalPosition")); + } + + @Test + public void shouldReturnRowIdentifierDetailsWithNoPK() throws Exception { + // Given + WebSQLProcessor sqlProcessor = WebServiceBindingSQL.getSQLProcessor(webConnectionInfo); + WebSQLContextInfo sqlProcessorContext = sqlProcessor.createContext( + null, "PUBLIC", globalProject.getId() + ); + String taskId = clientWrapper.asyncSqlExecute( + globalProject, + sqlProcessorContext, + databaseContainer.getId(), + "SELECT * FROM NO_PK" + ); + clientWrapper.waitTaskCompleted(taskId); + + // When + Map response = clientWrapper.readTaskResultSet(taskId); + + // Then + String rowIdentifierState = String.valueOf(response.get("rowIdentifierState")); + Assertions.assertEquals( + WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState.NONE.name(), + rowIdentifierState + ); + Assertions.assertNull(response.get("rowIdentifier")); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/DBTestConstants.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/DBTestConstants.java new file mode 100644 index 0000000000..977fbeb97f --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/DBTestConstants.java @@ -0,0 +1,27 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.util; + + +public final class DBTestConstants { + public static final String H2_EMBEDDED_DRIVER_ID = "h2_embedded_v2"; + public static final String H2_EMBEDDED_DRIVER_ID_FULL = "h2:" + H2_EMBEDDED_DRIVER_ID; + public static final String H2_MEM_DB_URL = "jdbc:h2:mem:"; + + private DBTestConstants() { + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestClientWrapper.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestClientWrapper.java new file mode 100644 index 0000000000..fe6751cc2a --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestClientWrapper.java @@ -0,0 +1,187 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.util; + +import io.cloudbeaver.WebSessionGlobalProjectImpl; +import io.cloudbeaver.service.sql.WebSQLContextInfo; +import io.cloudbeaver.test.WebGQLClient; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.utils.CommonUtils; +import org.junit.jupiter.api.Assertions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static io.cloudbeaver.test.platform.util.GraphQLTestConstant.*; + + +public class GraphQLTestClientWrapper { + + @NotNull + private final WebGQLClient client; + + public GraphQLTestClientWrapper(@NotNull WebGQLClient client) { + this.client = client; + } + + @NotNull + public String readTaskResultId(@NotNull String taskId) throws Exception { + return readTaskResultId( + taskId, (list) -> { + } + ); + } + + @NotNull + public String readTaskResultId( + @NotNull String taskId, + @NotNull Consumer>> assertHandler + ) throws Exception { + Map resultSet = readTaskResultSet(taskId); + List> rowsWithMetaData = JSONUtils.getObjectList(resultSet, "rowsWithMetaData"); + assertHandler.accept(rowsWithMetaData); + return String.valueOf(resultSet.get("id")); + } + + @NotNull + public Map readTaskResultSet( + @NotNull String taskId + ) throws Exception { + Map resVars = new HashMap<>(); + resVars.put("taskId", taskId); + Map resResp = client.sendQuery(GQL_ASYNC_SQL_EXECUTE_RESULTS, resVars); + List> resultsList = JSONUtils.getObjectList(resResp, "results"); + Map firstResult = resultsList.getFirst(); + return JSONUtils.getObject(firstResult, "resultSet"); + } + + public void waitTaskCompleted(@NotNull String taskId) throws Exception { + Map taskInfoVars = new HashMap<>(); + taskInfoVars.put("id", taskId); + taskInfoVars.put("removeOnFinish", false); + String taskStatus = null; + int attempts = 0; + int maxAttempts = 20; + while (attempts++ < maxAttempts) { + Map taskInfo = client.sendQuery(GQL_ASYNC_TASK_INFO, taskInfoVars); + if (taskInfo != null) { + Object st = taskInfo.get("status"); + if (st != null) { + taskStatus = String.valueOf(st); + } + if (taskStatus != null) { + if (taskStatus.equalsIgnoreCase("FINISHED")) { + break; + } + Object error = taskInfo.get("error"); + if (error != null && CommonUtils.isNotEmpty(error.toString())) { + throw new IllegalStateException("Async task failed: " + error); + } + } + } + Thread.sleep(200); + } + } + + @NotNull + public String asyncReadDataFromContainer( + @NotNull WebSessionGlobalProjectImpl globalProject, + @NotNull WebSQLContextInfo sqlProcessorContext, + @NotNull String databaseContainerId, + @NotNull String nodePath, + @NotNull Map filter + ) throws Exception { + Map execVars = new HashMap<>(); + execVars.put("projectId", globalProject.getId()); + execVars.put("contextId", sqlProcessorContext.getId()); + execVars.put("connectionId", databaseContainerId); + execVars.put("containerNodePath", nodePath); + execVars.put("filter", filter); + Map readResp = client.sendQuery(GQL_ASYNC_READ_DATA_FROM_CONTAINER, execVars); + Assertions.assertNotNull(readResp); + Object taskIdObj = readResp.get("id"); + Assertions.assertNotNull(taskIdObj, "asyncReadDataFromContainer must return task id"); + return taskIdObj.toString(); + } + + @NotNull + public String asyncSqlExecute( + @NotNull WebSessionGlobalProjectImpl globalProject, + @NotNull WebSQLContextInfo sqlProcessorContext, + @NotNull String databaseContainerId, + @NotNull String sql + ) throws Exception { + Map execVars = new HashMap<>(); + execVars.put("projectId", globalProject.getId()); + execVars.put("connectionId", databaseContainerId); + execVars.put("contextId", sqlProcessorContext.getId()); + execVars.put("sql", sql); + Map readResp = client.sendQuery(GQL_ASYNC_SQL_EXECUTE, execVars); + Assertions.assertNotNull(readResp); + Object taskIdObj = readResp.get("id"); + Assertions.assertNotNull(taskIdObj, "asyncSqlExecute must return task id"); + return taskIdObj.toString(); + } + + public String openSession() throws Exception { + Map openVars = new HashMap<>(); + openVars.put("defaultLocale", "en"); + Map bodyAndHeaders = client.sendQueryAndGetHeaders(GQL_OPEN_SESSION, openVars, Map.of()); + Map headers = JSONUtils.getObject(bodyAndHeaders, "headers"); + String cookie = JSONUtils.getString(headers, "Set-Cookie"); + Map cookieMap = parseCookies(cookie); + return cookieMap.get("cb-session-id"); + } + + private Map parseCookies(String cookie) { + Map result = new HashMap<>(); + if (cookie == null) { + return result; + } + + String trimmedCookie = cookie.trim(); + // Handle cases when cookie string is wrapped in [ ... ] + if (trimmedCookie.startsWith("[") && trimmedCookie.endsWith("]") && trimmedCookie.length() >= 2) { + trimmedCookie = trimmedCookie.substring(1, trimmedCookie.length() - 1).trim(); + } + if (trimmedCookie.isEmpty()) { + return result; + } + + // Example: "cb-session-id=1as6ho3pnnonsmeh02uxevmhu0; Path=/; Expires=...; HttpOnly" + String[] parts = trimmedCookie.split(";\\s*"); + for (String part : parts) { + String p = part.trim(); + if (p.isEmpty()) { + continue; + } + int eqPos = p.indexOf('='); + if (eqPos <= 0) { + // attributes like HttpOnly, Secure, etc. (no '=') + continue; + } + String name = p.substring(0, eqPos); + String value = p.substring(eqPos + 1); + result.put(name, value); + } + return result; + } + +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestConstant.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestConstant.java new file mode 100644 index 0000000000..668487ac33 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/GraphQLTestConstant.java @@ -0,0 +1,339 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.util; + +public class GraphQLTestConstant { + + public static final String GQL_SQL_GROUPING_RESULTSET = """ + query asyncSqlGroupingResultSet($projectId: ID, $contextId: ID!, $connectionId: ID!, $resultsId: ID!, $columnNames: [String!], $functions: [String!], $showDuplicatesOnly: Boolean, $filter: SQLDataFilter, $dataFormat: ResultDataFormat, $isInteractive: Boolean) { + result: asyncSqlGroupingResultSet(projectId: $projectId, contextId: $contextId, connectionId: $connectionId, originalResultsId: $resultsId, columnNames: $columnNames, functions: $functions, showDuplicatesOnly: $showDuplicatesOnly, filter: $filter, dataFormat: $dataFormat, isInteractive: $isInteractive) { + id + } + } + """; + + public static final String GQL_OPEN_SESSION = """ + mutation openSession($defaultLocale: String) { + result: openSession(defaultLocale: $defaultLocale) { + createTime + lastAccessTime + cacheExpired + locale + actionParameters + valid + remainingTime + } + } + """; + + public static final String GQL_ASYNC_TASK_INFO = """ + mutation asyncTaskInfo($id: String!, $removeOnFinish: Boolean!) { + result: asyncTaskInfo(id: $id, removeOnFinish: $removeOnFinish) { + id + name + running + status + error { message errorCode errorType } + taskResult + } + } + """; + + public static final String GQL_ASYNC_SQL_EXECUTE_RESULTS = """ + mutation asyncSqlExecuteResults($taskId: ID!) { + result: asyncSqlExecuteResults(taskId: $taskId) { + results { + resultSet { + id + readOnly + rowIdentifierState + rowIdentifier { + attributes { + name + ordinalPosition + } + constraintType + } + columns { + dataKind + entityName + fullTypeName + icon + description + label + maxLength + name + position + precision + required + autoGenerated + readOnly + readOnlyStatus + scale + typeName + supportedOperations { + id + expression + argumentCount + } + } + associations { + associationName + targetEntityName + nodePath + columnIndexList + } + references { + associationName + targetEntityName + nodePath + columnIndexList + } + rowsWithMetaData { + data + metaData + } + } + } + } + } + """; + + public static final String GQL_ASYNC_SQL_NAVIGATE_FOREIGN_KEY = """ + mutation asyncSqlNavigateForeignKey( + $projectId: ID, + $connectionId: ID!, + $contextId: ID!, + $resultsId: ID!, + $row: SQLResultRow!, + $columnIndex: Int!, + $associationName: String!, + $isReference: Boolean!, + $dataFormat: ResultDataFormat + ) { + result: asyncSqlNavigateForeignKey( + projectId: $projectId, + connectionId: $connectionId, + contextId: $contextId, + resultsId: $resultsId, + row: $row, + columnIndex: $columnIndex, + associationName: $associationName, + isReference: $isReference, + dataFormat: $dataFormat + ) { + id + } + } + """; + + public static final String GQL_ASYNC_SQL_EXECUTE = """ + mutation asyncSqlExecuteQuery($projectId: ID, $connectionId: ID!, $contextId: ID!, $sql: String!) { + result: asyncSqlExecuteQuery(projectId: $projectId, connectionId: $connectionId, contextId: $contextId, sql: $sql) { + id + } + } + """; + + public static final String GQL_SQL_CONTEXT_CREATE = """ + mutation sqlContextCreate($projectId: ID, $connectionId: ID!) { + result: sqlContextCreate(projectId: $projectId, connectionId: $connectionId) { + id + projectId + connectionId + } + } + """; + + public static final String GQL_ASYNC_SQL_EXPLAIN_EXECUTION_PLAN = """ + mutation asyncSqlExplainExecutionPlan( + $projectId: ID!, + $connectionId: ID!, + $contextId: ID!, + $query: String!, + $configuration: Object! + ) { + result: asyncSqlExplainExecutionPlan( + projectId: $projectId, + connectionId: $connectionId, + contextId: $contextId, + query: $query, + configuration: $configuration + ) { + id + running + status + error { + message + errorCode + errorType + } + } + } + """; + + public static final String GQL_ASYNC_SQL_EXPLAIN_EXECUTION_PLAN_RESULT = """ + mutation asyncSqlExplainExecutionPlanResult($taskId: ID!) { + result: asyncSqlExplainExecutionPlanResult(taskId: $taskId) { + query + hasCost + hasRows + hasDuration + durationMeasure + nodes { + id + parentId + kind + name + type + condition + description + cost + rowCount + duration + percent + } + } + } + """; + + public static final String GQL_ASYNC_READ_DATA_FROM_CONTAINER = """ + mutation asyncReadDataFromContainer( + $projectId: ID, + $connectionId: ID!, + $contextId: ID!, + $containerNodePath: ID!, + $filter: SQLDataFilter + ) { + result: asyncReadDataFromContainer( + projectId: $projectId, + connectionId: $connectionId, + contextId: $contextId, + containerNodePath: $containerNodePath, + filter: $filter + ) { + id + } + } + """; + + public static final String GQL_CREATE_USER = """ + query createUser($userId: ID!, $enabled: Boolean!, $authRole: String) { + result: createUser(userId: $userId, enabled: $enabled, authRole: $authRole) { userId } + }"""; + + public static final String GQL_DELETE_USER = """ + query deleteUser($userId: ID!) { + result: deleteUser(userId: $userId) + }"""; + + public static final String GQL_SET_USER_CREDENTIALS = """ + query setUserCredentials($userId: ID!, $providerId: ID!, $credentials: Object!) { + result: setUserCredentials(userId: $userId, providerId: $providerId, credentials: $credentials) + }"""; + + public static final String GQL_ADMIN_USER_INFO = """ + query adminUserInfo($userId: ID!) { + result: adminUserInfo(userId: $userId) { + userId + enabled + authRole + metaParameters + configurationParameters + grantedTeams + linkedAuthProviders + grantedConnections { + connectionId + dataSourceId + subjectId + subjectType + } + origins { + type + subType + displayName + icon + configuration + details { + id + displayName + description + category + dataType + value + defaultValue + } + } + disableDate + disabledBy + disableReason + lastLoginTime + } + }"""; + + public static final String GQL_CREATE_CONNECTION = """ + mutation createConnection($config: ConnectionConfig!, $projectId: ID) { + result: createConnection(config: $config, projectId: $projectId) { id } + }"""; + + public static final String GQL_UPDATE_CONNECTION = """ + mutation updateConnection($projectId: ID!, $config: ConnectionConfig!) { + result: updateConnection(projectId: $projectId, config: $config) { id defaultUserPreferences } + }"""; + + public static final String GQL_GET_CONN_SETTINGS = """ + query getUserConnectionsAuthProperties( + $projectId: ID + $connectionId: ID + $projectIds: [ID!] + ) { + result: userConnections( + projectId: $projectId + id: $connectionId + projectIds: $projectIds + ) { + id, + name, + defaultUserPreferences + } + }"""; + + public static final String GQL_DELETE_CONNECTION = """ + mutation deleteConnection($id: ID!, $projectId: ID) { + result: deleteConnection(id: $id, projectId: $projectId) + }"""; + + public static final String GQL_ADD_CONN_ACCESS = """ + query addConnectionsAccess($projectId: ID!, $connectionIds: [ID!]!, $subjects: [ID!]!) { + result: addConnectionsAccess(projectId: $projectId, connectionIds: $connectionIds, subjects: $subjects) + }"""; + + public static final String GQL_SET_OBJECT_SETTINGS = """ + mutation SetObjectSettingsForDatasource($id: ID!, $projectId: ID, $settings: Object!) { + result: setObjectSettingsForDatasource(id: $id, projectId: $projectId, settings: $settings) + }"""; + + public static final String GQL_USER_CONNECTIONS = """ + query userConnections { + result: userConnections { + id + name + driverId + projectId + } + }"""; +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/WebDBTestUtils.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/WebDBTestUtils.java new file mode 100644 index 0000000000..d8d960097f --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/util/WebDBTestUtils.java @@ -0,0 +1,67 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.test.platform.util; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; + +public class WebDBTestUtils { + + private static final String DRIVER_ID = DBTestConstants.H2_EMBEDDED_DRIVER_ID; + + @NotNull + public static DBPDataSourceContainer createH2DataSource( + @NotNull DBRProgressMonitor monitor, + @NotNull DBPProject project + ) throws DBException { + return createH2DataSource(monitor, project, DBTestConstants.H2_MEM_DB_URL); + } + + @NotNull + public static DBPDataSourceContainer createH2DataSource( + @NotNull DBRProgressMonitor monitor, + @NotNull DBPProject project, + @NotNull String url + ) throws DBException { + final DBPDriver driver = DataSourceProviderRegistry.getInstance().findDriver(DRIVER_ID); + if (driver == null) { + throw new DBException("Could not find H2 driver: " + DRIVER_ID); + } + + DBPConnectionConfiguration configuration = new DBPConnectionConfiguration(); + configuration.setUrl(url); + + DataSourceDescriptor dataSourceDescriptor = project.getDataSourceRegistry().createDataSource( + DataSourceDescriptor.generateNewId(driver), + driver, + configuration + ); + dataSourceDescriptor.setName("H2 DB" + System.currentTimeMillis()); + dataSourceDescriptor.setSavePassword(true); + dataSourceDescriptor.setTemporary(true); + dataSourceDescriptor.connect(monitor, true, true); + + return dataSourceDescriptor; + } +} From 15b880240a9776ac60f23c8ec1eb73d981b6ef9e Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 17 Jun 2026 12:11:03 +0200 Subject: [PATCH 2/5] dbeaver/pro#9568 move tests to ce --- .../io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java index 15f108188b..6aba976bd5 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/GroupingEndpointTest.java @@ -173,7 +173,7 @@ private Map callGroupingSql( groupVars.put("connectionId", databaseContainer.getId()); groupVars.put("resultsId", resultId); groupVars.put("columnNames", columnNames); - groupVars.put("functions", "COUNT(*)"); + groupVars.put("functions", List.of("COUNT(*)")); groupVars.put("showDuplicatesOnly", false); groupVars.put("filter", null); groupVars.put("dataFormat", null); From 9f0a9b1d1c8884bb6ce276f5ec22a781b451846f Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 17 Jun 2026 15:55:23 +0200 Subject: [PATCH 3/5] dbeaver/pro#9568 fix tests --- server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF | 3 ++- .../workspace/conf/cloudbeaver.conf | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF index 17af949298..7b59454d2d 100644 --- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF +++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF @@ -34,4 +34,5 @@ Require-Bundle: org.eclipse.core.runtime, org.jkiss.dbeaver.ext.generic, org.eclipse.lsp4j, org.eclipse.lsp4j.jsonrpc -Export-Package: io.cloudbeaver.test.platform +Export-Package: io.cloudbeaver.test.platform, + io.cloudbeaver.test.platform.util diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf index 31dd612887..70e665076a 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf @@ -61,7 +61,9 @@ disabledDrivers: [ "sqlite:sqlite_jdbc", - "h2:h2_embedded", + "h2:h2_embedded" + ], + enabledDrivers: [ "h2:h2_embedded_v2" ] } From 1514fd775322bbaf1cbcbc3d03d502295e51989f Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 17 Jun 2026 16:45:06 +0200 Subject: [PATCH 4/5] dbeaver/pro#9568 fix tests --- .../sql/DataFilterConstraintsTest.java | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java index 43c97032b2..744e5e7890 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java @@ -21,6 +21,7 @@ import io.cloudbeaver.service.sql.WebServiceBindingSQL; import io.cloudbeaver.test.platform.CloudbeaverDBTest; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; import org.junit.jupiter.api.Assertions; @@ -34,6 +35,23 @@ public class DataFilterConstraintsTest extends CloudbeaverDBTest { + private static final String GQL_NAV_STRUCT_CONTAINERS = """ + query navGetStructContainers($projectId: ID, $connectionId: ID!) { + result: navGetStructContainers(projectId: $projectId, connectionId: $connectionId) { + parentNode { uri } + } + }"""; + + private static final String GQL_NAV_NODE_CHILDREN = """ + query navNodeChildren($parentPath: ID!) { + result: navNodeChildren(parentPath: $parentPath) { + uri + name + folder + hasChildren + } + }"""; + private static WebSQLContextInfo sqlProcessorContext; @BeforeEach @@ -149,12 +167,53 @@ public void shouldApplyIsNullsDataFilter() throws Exception { Assertions.assertFalse(responseJson.contains("value_3")); } + /** + * Resolves the real navigator node URI of the table by browsing the tree, instead of guessing + * the path. Browsing also materializes the node so that {@code asyncReadDataFromContainer} can + * resolve it on the server side. + */ @NotNull - private String resolveNodePath() { - return String.format( - "database://%s/PUBLIC/org.jkiss.dbeaver.ext.h2.model.H2Table/TEST_TABLE", - databaseContainer.getId() + private String resolveNodePath() throws Exception { + String connectionNodeUri = findConnectionNodeUri(); + String tableNodeUri = findNodeUriByName(connectionNodeUri, "TEST_TABLE", 5); + Assertions.assertNotNull(tableNodeUri, "TEST_TABLE navigator node not found"); + return tableNodeUri; + } + + @NotNull + private String findConnectionNodeUri() throws Exception { + Map containers = client.sendQuery( + GQL_NAV_STRUCT_CONTAINERS, + Map.of("projectId", globalProject.getId(), "connectionId", databaseContainer.getId()) ); + Assertions.assertNotNull(containers); + String uri = JSONUtils.getString(JSONUtils.getObject(containers, "parentNode"), "uri"); + Assertions.assertNotNull(uri, "Connection navigator node not found"); + return uri; + } + + @Nullable + private String findNodeUriByName(@NotNull String parentUri, @NotNull String name, int maxDepth) throws Exception { + List> children = client.sendQuery(GQL_NAV_NODE_CHILDREN, Map.of("parentPath", parentUri)); + if (children == null) { + return null; + } + for (Map child : children) { + if (name.equals(JSONUtils.getString(child, "name"))) { + return JSONUtils.getString(child, "uri"); + } + } + if (maxDepth > 0) { + for (Map child : children) { + if (JSONUtils.getBoolean(child, "folder") || JSONUtils.getBoolean(child, "hasChildren")) { + String found = findNodeUriByName(JSONUtils.getString(child, "uri"), name, maxDepth - 1); + if (found != null) { + return found; + } + } + } + } + return null; } } From 1570cfebd7f81c4cd558545dc5c84989114f0bec Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 17 Jun 2026 18:06:44 +0200 Subject: [PATCH 5/5] dbeaver/pro#9568 fix tests --- .../sql/DataFilterConstraintsTest.java | 94 +++++++++---------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java index 744e5e7890..f2f6a14f60 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/sql/DataFilterConstraintsTest.java @@ -22,12 +22,22 @@ 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.DBUtils; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement; +import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode; +import org.jkiss.dbeaver.model.navigator.DBNModel; +import org.jkiss.dbeaver.model.navigator.DBNProject; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.struct.DBSObject; +import org.jkiss.dbeaver.model.struct.DBSObjectContainer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,23 +45,6 @@ public class DataFilterConstraintsTest extends CloudbeaverDBTest { - private static final String GQL_NAV_STRUCT_CONTAINERS = """ - query navGetStructContainers($projectId: ID, $connectionId: ID!) { - result: navGetStructContainers(projectId: $projectId, connectionId: $connectionId) { - parentNode { uri } - } - }"""; - - private static final String GQL_NAV_NODE_CHILDREN = """ - query navNodeChildren($parentPath: ID!) { - result: navNodeChildren(parentPath: $parentPath) { - uri - name - folder - hasChildren - } - }"""; - private static WebSQLContextInfo sqlProcessorContext; @BeforeEach @@ -167,49 +160,48 @@ public void shouldApplyIsNullsDataFilter() throws Exception { Assertions.assertFalse(responseJson.contains("value_3")); } - /** - * Resolves the real navigator node URI of the table by browsing the tree, instead of guessing - * the path. Browsing also materializes the node so that {@code asyncReadDataFromContainer} can - * resolve it on the server side. - */ @NotNull private String resolveNodePath() throws Exception { - String connectionNodeUri = findConnectionNodeUri(); - String tableNodeUri = findNodeUriByName(connectionNodeUri, "TEST_TABLE", 5); - Assertions.assertNotNull(tableNodeUri, "TEST_TABLE navigator node not found"); - return tableNodeUri; - } + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + DBNModel navigatorModel = webSession.getNavigatorModelOrThrow(); - @NotNull - private String findConnectionNodeUri() throws Exception { - Map containers = client.sendQuery( - GQL_NAV_STRUCT_CONTAINERS, - Map.of("projectId", globalProject.getId(), "connectionId", databaseContainer.getId()) - ); - Assertions.assertNotNull(containers); - String uri = JSONUtils.getString(JSONUtils.getObject(containers, "parentNode"), "uri"); - Assertions.assertNotNull(uri, "Connection navigator node not found"); - return uri; + DBNProject projectNode = navigatorModel.getRoot().getProjectNode(globalProject); + Assertions.assertNotNull(projectNode, "Project navigator node not found"); + projectNode.getDatabases().getChildren(monitor); + + DBSObjectContainer rootContainer = DBUtils.getAdapter(DBSObjectContainer.class, webConnectionInfo.getDataSource()); + Assertions.assertNotNull(rootContainer, "Connection is not a database object container"); + DBSEntity table = findEntity(monitor, rootContainer, "TEST_TABLE", 4); + Assertions.assertNotNull(table, "TEST_TABLE entity not found"); + + DBNDatabaseNode tableNode = navigatorModel.getNodeByObject(monitor, table, true); + Assertions.assertNotNull(tableNode, "Navigator node for TEST_TABLE not found"); + return tableNode.getNodeUri(); } @Nullable - private String findNodeUriByName(@NotNull String parentUri, @NotNull String name, int maxDepth) throws Exception { - List> children = client.sendQuery(GQL_NAV_NODE_CHILDREN, Map.of("parentPath", parentUri)); - if (children == null) { + private DBSEntity findEntity( + @NotNull DBRProgressMonitor monitor, + @NotNull DBSObjectContainer container, + @NotNull String name, + int depth + ) throws DBException { + DBSObject direct = container.getChild(monitor, name); + if (direct instanceof DBSEntity entity) { + return entity; + } + if (depth <= 0) { return null; } - for (Map child : children) { - if (name.equals(JSONUtils.getString(child, "name"))) { - return JSONUtils.getString(child, "uri"); - } + Collection children = container.getChildren(monitor); + if (children == null) { + return null; } - if (maxDepth > 0) { - for (Map child : children) { - if (JSONUtils.getBoolean(child, "folder") || JSONUtils.getBoolean(child, "hasChildren")) { - String found = findNodeUriByName(JSONUtils.getString(child, "uri"), name, maxDepth - 1); - if (found != null) { - return found; - } + for (DBSObject child : children) { + if (child instanceof DBSObjectContainer sub) { + DBSEntity found = findEntity(monitor, sub, name, depth - 1); + if (found != null) { + return found; } } }