diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml
index 38a95c5451b..20f40d13a88 100644
--- a/server/features/io.cloudbeaver.server.feature/feature.xml
+++ b/server/features/io.cloudbeaver.server.feature/feature.xml
@@ -49,5 +49,6 @@
+
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 031a387de73..405ae54bedc 100644
--- a/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF
+++ b/server/test/io.cloudbeaver.test.platform/META-INF/MANIFEST.MF
@@ -14,6 +14,8 @@ Require-Bundle: org.eclipse.core.runtime,
org.jkiss.dbeaver.model,
org.jkiss.dbeaver.osgi.test.runner;visibility:=reexport,
org.jkiss.dbeaver.model.sql,
+ org.jkiss.dbeaver.model.lsp,
+ org.jkiss.dbeaver.model.jdbc,
org.jkiss.dbeaver.registry,
org.jkiss.dbeaver.ext.generic,
org.jkiss.dbeaver.ext.h2,
@@ -30,5 +32,9 @@ Require-Bundle: org.eclipse.core.runtime,
org.jkiss.dbeaver.ext.mysql,
org.jkiss.dbeaver.ext.postgresql,
org.jkiss.dbeaver.ext.oracle,
- org.jkiss.dbeaver.ext.mssql
+ org.jkiss.dbeaver.ext.mssql,
+ org.jkiss.dbeaver.ext.h2,
+ org.jkiss.dbeaver.ext.generic,
+ org.eclipse.lsp4j,
+ org.eclipse.lsp4j.jsonrpc
Export-Package: io.cloudbeaver.test.platform
diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceContextTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceContextTest.java
new file mode 100644
index 00000000000..783fdf8616e
--- /dev/null
+++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceContextTest.java
@@ -0,0 +1,131 @@
+/*
+ * DBeaver - Universal Database Manager
+ * Copyright (C) 2010-2025 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.model.lsp;
+
+import org.eclipse.lsp4j.*;
+import org.jkiss.dbeaver.ext.h2.model.H2SQLDialect;
+import org.jkiss.dbeaver.model.lsp.context.ContextAwareDocument;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+public class DBLTextDocumentServiceContextTest extends H2DataSourceTest {
+
+ @Test
+ public void shouldInitH2Context() {
+ TextDocumentItem document = DocumentServiceTestUtils.createAndSaveDocument(
+ service, "select * from table", project.getId(), DocumentServiceTestUtils.BASIC_RESOURCE_PATH
+ );
+
+ ContextAwareDocument contextedDocument = DocumentServiceTestUtils.getDocument(service, document.getUri());
+ Assert.assertNotNull(contextedDocument);
+ Assert.assertEquals(dataSourceDescriptor.getDataSource(), contextedDocument.getDataSource());
+ Assert.assertNotNull(contextedDocument.getExecutionContext());
+ Assert.assertEquals(dataSourceDescriptor.getDataSource(), contextedDocument.getExecutionContext().getDataSource());
+ Assert.assertTrue(contextedDocument.getSyntaxManager().getDialect() instanceof H2SQLDialect);
+ Assert.assertNotNull(contextedDocument.getRuleManager());
+ }
+
+ @Test
+ public void shouldFormatQuery() throws ExecutionException, InterruptedException {
+ String query = """
+ INSERT INTO users (id, profile) VALUES (1,'{"name": "JohnDoe"}'::jsonb) ON CONFLICT (id)
+ DO UPDATE SET profile = users.profile || EXCLUDED.profile RETURNING id, profile->>'name' AS name;
+ """.trim();
+ DocumentFormattingParams formattingParams = DocumentServiceTestUtils.setupDocumentAndBuildFormattingParams(service, query);
+
+ CompletableFuture> future = service.formatting(formattingParams);
+
+ TextEdit edit = future.get().getFirst();
+ String expectedQuery = """
+ INSERT
+ INTO
+ users (id,
+ profile)
+ VALUES (1,
+ '{"name": "JohnDoe"}'::jsonb) ON
+ CONFLICT (id)
+ DO
+ UPDATE
+ SET
+ profile = users.profile || EXCLUDED.profile RETURNING id,
+ profile->>'name' AS name;
+ """.trim();
+
+ Assert.assertEquals(expectedQuery.trim(), edit.getNewText());
+ Position start = edit.getRange().getStart();
+ Assert.assertEquals(0, start.getLine());
+ Assert.assertEquals(0, start.getCharacter());
+
+ Position end = edit.getRange().getEnd();
+ Assert.assertEquals(1, end.getLine());
+ Assert.assertEquals(97, end.getCharacter());
+ }
+
+ @Test
+ public void shouldReturnEmptyCompletionsForInvalidPosition() throws ExecutionException, InterruptedException {
+ String query = "SEL";
+ ContextAwareDocument document = DocumentServiceTestUtils.createAndSaveDocument(
+ service, query, project.getId(), DocumentServiceTestUtils.BASIC_RESOURCE_PATH
+ );
+ TextDocumentIdentifier documentId = new TextDocumentIdentifier(document.getUri());
+ CompletionParams completionParams = new CompletionParams(documentId, new Position(1, 42));
+
+ CompletionList completions = service.completion(completionParams).get().getRight();
+
+ Assert.assertNotNull(completions);
+ Assert.assertTrue(completions.getItems().isEmpty());
+ }
+
+ @Test
+ public void shouldSuggestKeywordCompletion() throws ExecutionException, InterruptedException {
+ String query = "SEL";
+ ContextAwareDocument document = DocumentServiceTestUtils.createAndSaveDocument(
+ service, query, project.getId(), DocumentServiceTestUtils.BASIC_RESOURCE_PATH
+ );
+ TextDocumentIdentifier documentId = new TextDocumentIdentifier(document.getUri());
+ CompletionParams completionParams = new CompletionParams(documentId, new Position(0, 3));
+
+ CompletionList completions = service.completion(completionParams).get().getRight();
+
+ Assert.assertNotNull(completions);
+ Assert.assertFalse(completions.getItems().isEmpty());
+ Assert.assertEquals("SELECT", completions.getItems().getFirst().getLabel());
+ }
+
+ @Test
+ public void shouldSuggestMultilineKeywordCompletion() throws ExecutionException, InterruptedException {
+ String query = """
+ SELECT *
+ FR
+ """;
+ ContextAwareDocument document = DocumentServiceTestUtils.createAndSaveDocument(
+ service, query, project.getId(), DocumentServiceTestUtils.BASIC_RESOURCE_PATH
+ );
+ TextDocumentIdentifier documentId = new TextDocumentIdentifier(document.getUri());
+ CompletionParams completionParams = new CompletionParams(documentId, new Position(1, 6));
+
+ CompletionList completions = service.completion(completionParams).get().getRight();
+
+ Assert.assertNotNull(completions);
+ Assert.assertEquals(1, completions.getItems().size());
+ Assert.assertEquals("FROM", completions.getItems().getFirst().getLabel());
+ }
+}
diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceTest.java
new file mode 100644
index 00000000000..8aac7f0da8d
--- /dev/null
+++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceTest.java
@@ -0,0 +1,294 @@
+/*
+ * DBeaver - Universal Database Manager
+ * Copyright (C) 2010-2025 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.model.lsp;
+
+import io.cloudbeaver.CloudbeaverMockTest;
+import org.eclipse.lsp4j.*;
+import org.jkiss.dbeaver.model.impl.sql.BasicSQLDialect;
+import org.jkiss.dbeaver.model.lsp.DBLTextDocumentService;
+import org.jkiss.dbeaver.model.lsp.context.ContextAwareDocument;
+import org.jkiss.dbeaver.model.sql.SQLSyntaxManager;
+import org.jkiss.dbeaver.model.sql.parser.SQLRuleManager;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Test scenarios to cover DBLTextDocumentService
+ */
+public class DBLTextDocumentServiceTest extends CloudbeaverMockTest {
+ private DBLTextDocumentService service;
+
+ @Before
+ public void setUp() {
+ service = new DBLTextDocumentService();
+ }
+
+ @Test
+ public void shouldOpenDocumentWithArbitraryURI() {
+ String query = "SELECT * FROM table";
+ String uri = "file:///Users/username/script.sql";
+ TextDocumentItem textDocument = new TextDocumentItem(
+ uri, DocumentServiceTestUtils.SQL_LANGUAGE_ID, 0, query
+ );
+ DidOpenTextDocumentParams params = new DidOpenTextDocumentParams(textDocument);
+
+ service.didOpen(params);
+
+ ContextAwareDocument document = DocumentServiceTestUtils.getDocument(service, uri);
+ Assert.assertNotNull(document);
+ Assert.assertEquals(document.getSyntaxManager().getDialect(), BasicSQLDialect.INSTANCE);
+ Assert.assertNull(document.getExecutionContext());
+ }
+
+ @Test
+ public void shouldOpenDocument() {
+ String query = "SELECT * FROM table";
+ TextDocumentItem textDocument = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(textDocument));
+
+ ContextAwareDocument savedDocument = DocumentServiceTestUtils.getDocument(service, textDocument.getUri());
+ Assert.assertNotNull(savedDocument);
+ Assert.assertEquals(query, savedDocument.getText());
+
+ SQLSyntaxManager syntaxManager = savedDocument.getSyntaxManager();
+ Assert.assertNotNull(syntaxManager);
+ Assert.assertEquals(BasicSQLDialect.INSTANCE, syntaxManager.getDialect());
+ SQLRuleManager ruleManager = savedDocument.getRuleManager();
+ Assert.assertNotNull(ruleManager);
+ }
+
+ @Test
+ public void shouldInitDefaultSyntax() {
+ TextDocumentItem textDocument = DocumentServiceTestUtils.createQueryDocument("SELECT * FROM table");
+ service.didOpen(new DidOpenTextDocumentParams(textDocument));
+
+ ContextAwareDocument savedDocument = Objects.requireNonNull(DocumentServiceTestUtils.getDocument(service, textDocument.getUri()));
+ SQLSyntaxManager syntaxManager = savedDocument.getSyntaxManager();
+ Assert.assertNotNull(syntaxManager);
+ Assert.assertEquals(BasicSQLDialect.INSTANCE, syntaxManager.getDialect());
+ SQLRuleManager ruleManager = savedDocument.getRuleManager();
+ Assert.assertNotNull(ruleManager);
+ }
+
+ @Test
+ public void shouldOpenAndChangeDocument() {
+ String query = "SELECT * FROM table";
+ TextDocumentItem textDocument = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(textDocument));
+
+ String updatedSql = "SELECT DISTINCT * FROM table";
+ VersionedTextDocumentIdentifier textDocumentChange = new VersionedTextDocumentIdentifier(textDocument.getUri(), 0);
+ TextDocumentContentChangeEvent event = new TextDocumentContentChangeEvent(updatedSql);
+ List contentChanges = List.of(event);
+ service.didChange(new DidChangeTextDocumentParams(textDocumentChange, contentChanges));
+
+ ContextAwareDocument updatedDocument = DocumentServiceTestUtils.getDocument(service, textDocument.getUri());
+ Assert.assertNotNull(updatedDocument);
+ Assert.assertEquals(updatedSql, updatedDocument.getText());
+ }
+
+ @Test
+ public void shouldFailSubmittingMultipleChangesToDocument() {
+ String query = "SELECT * FROM table";
+ TextDocumentItem textDocument = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(textDocument));
+
+ String updatedSql1 = "SELECT DISTINCT * FROM table";
+ String updatedSql2 = "DROP TABLE IF EXISTS table";
+ VersionedTextDocumentIdentifier textDocumentChange = new VersionedTextDocumentIdentifier(textDocument.getUri(), 0);
+ TextDocumentContentChangeEvent event1 = new TextDocumentContentChangeEvent(updatedSql1);
+ TextDocumentContentChangeEvent event2 = new TextDocumentContentChangeEvent(updatedSql2);
+ List contentChanges = List.of(event1, event2);
+
+ Assert.assertThrows(
+ "Unexpected number of document changes: 2",
+ IllegalArgumentException.class,
+ () -> service.didChange(new DidChangeTextDocumentParams(textDocumentChange, contentChanges))
+ );
+ }
+
+ @Test
+ public void shouldOpenAndCloseDocument() {
+ String query = "SELECT * FROM table";
+ TextDocumentItem textDocument = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(textDocument));
+
+ TextDocumentIdentifier textDocumentId = new TextDocumentIdentifier(textDocument.getUri());
+ DidCloseTextDocumentParams closeParams = new DidCloseTextDocumentParams(textDocumentId);
+ service.didClose(closeParams);
+
+ ContextAwareDocument updatedDocument = DocumentServiceTestUtils.getDocument(service, textDocument.getUri());
+ Assert.assertNull(updatedDocument);
+ }
+
+ @Test
+ public void shouldFormatSingleLineQuery() throws ExecutionException, InterruptedException {
+ String query = "sElEcT dIsTiNcT * fRoM tablename As alias;";
+ var formattingParams = DocumentServiceTestUtils.setupDocumentAndBuildFormattingParams(service, query);
+
+ CompletableFuture> future = service.formatting(formattingParams);
+
+ TextEdit textEdit = future.get().getFirst();
+ String expectedQuery = """
+ SELECT
+ DISTINCT *
+ FROM
+ tablename AS alias;
+ """;
+ Assert.assertEquals(expectedQuery.trim(), textEdit.getNewText());
+
+ Position start = textEdit.getRange().getStart();
+ Assert.assertEquals(0, start.getCharacter());
+ Assert.assertEquals(0, start.getLine());
+
+ Position end = textEdit.getRange().getEnd();
+ Assert.assertEquals(0, end.getLine());
+ Assert.assertEquals(42, end.getCharacter());
+ }
+
+ @Test
+ public void shouldFormatMultilineQuery() throws ExecutionException, InterruptedException {
+ String query = """
+ select dbname1.schemaname1.tablename1.columnname1, schemaname2.tablename2.columnname2,
+ tablename3.columnname3 from
+ dbname1.schemaname1.tablename1,dbname2.schemaname2.tablename2,schemaname3.tablename3
+ ;
+ """.trim();
+ var formattingParams = DocumentServiceTestUtils.setupDocumentAndBuildFormattingParams(service, query);
+
+ CompletableFuture> future = service.formatting(formattingParams);
+
+ TextEdit textEdit = future.get().getFirst();
+ String expectedQuery = """
+ SELECT
+ dbname1.schemaname1.tablename1.columnname1,
+ schemaname2.tablename2.columnname2,
+ tablename3.columnname3
+ FROM
+ dbname1.schemaname1.tablename1,
+ dbname2.schemaname2.tablename2,
+ schemaname3.tablename3
+ ;
+ """.trim();
+ Assert.assertEquals(expectedQuery.trim(), textEdit.getNewText());
+
+ Position start = textEdit.getRange().getStart();
+ Assert.assertEquals(0, start.getCharacter());
+ Assert.assertEquals(0, start.getLine());
+
+ Position end = textEdit.getRange().getEnd();
+ Assert.assertEquals(3, end.getLine());
+ Assert.assertEquals(1, end.getCharacter());
+ }
+
+ @Test
+ public void shouldFormatDialectSpecificQuery() throws ExecutionException, InterruptedException {
+ String query = """
+ DO $$ BEGIN CREATE TABLE logs (id serial PRIMARY KEY,message text,created_at timestamptz DEFAULT now());
+ ELSE RAISE NOTICE 'Table "logs" already exists.';END IF;END $$;
+ """.trim();
+ var formattingParams = DocumentServiceTestUtils.setupDocumentAndBuildFormattingParams(service, query);
+
+ CompletableFuture> future = service.formatting(formattingParams);
+
+ TextEdit textEdit = future.get().getFirst();
+ String expectedQuery = """
+ DO $$
+ BEGIN
+ CREATE TABLE logs (id serial PRIMARY KEY,
+ message text,
+ created_at timestamptz DEFAULT now());
+ ELSE RAISE NOTICE 'Table "logs" already exists.';
+ END IF;
+ END $$;
+ """.trim();
+
+ Assert.assertEquals(expectedQuery.trim(), textEdit.getNewText());
+ Position end = textEdit.getRange().getEnd();
+ Assert.assertEquals(1, end.getLine());
+ Assert.assertEquals(63, end.getCharacter());
+ }
+
+ @Test
+ public void shouldReturnKeywordTokenData() throws ExecutionException, InterruptedException {
+ String query = "SELECT";
+ TextDocumentItem document = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(document));
+
+ SemanticTokensParams params = new SemanticTokensParams(new TextDocumentIdentifier(document.getUri()));
+ Integer[] tokensData = service.semanticTokensFull(params).get().getData().toArray(new Integer[0]);
+
+ Assert.assertArrayEquals(
+ new Integer[] {0, 0, 6, 0, 0},
+ tokensData
+ );
+ }
+
+ @Test
+ public void shouldReturnMultipleTokensData() throws ExecutionException, InterruptedException {
+ String query = "SELECT name FROM users WHERE surname = 'Doe'";
+ TextDocumentItem document = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(document));
+
+ SemanticTokensParams params = new SemanticTokensParams(new TextDocumentIdentifier(document.getUri()));
+ Integer[] tokensData = service.semanticTokensFull(params).get().getData().toArray(new Integer[0]);
+
+ Integer[] expectedData = {
+ 0, 0, 6, 0, 0, // SELECT
+ 0, 12, 4, 0, 0, // FROM
+ 0, 23, 5, 0, 0, // WHERE
+ 0, 39, 5, 1, 0 // 'Doe'
+ };
+ Assert.assertArrayEquals(
+ expectedData,
+ tokensData
+ );
+ }
+
+ @Test
+ public void shouldReturnMultilineTokensData() throws ExecutionException, InterruptedException {
+ String query = """
+ SELECT name
+ FROM
+ users
+ WHERE
+ surname = 'Doe';
+ """;
+ TextDocumentItem document = DocumentServiceTestUtils.createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(document));
+
+ SemanticTokensParams params = new SemanticTokensParams(new TextDocumentIdentifier(document.getUri()));
+ Integer[] tokensData = service.semanticTokensFull(params).get().getData().toArray(new Integer[0]);
+
+ Integer[] expectedData = {
+ 0, 0, 6, 0, 0, // SELECT
+ 1, 0, 4, 0, 0, // FROM
+ 3, 0, 5, 0, 0, // WHERE
+ 4, 14, 5, 1, 0 // 'Doe'
+ };
+ Assert.assertArrayEquals(
+ expectedData,
+ tokensData
+ );
+ }
+}
diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceWorkspaceTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceWorkspaceTest.java
new file mode 100644
index 00000000000..6ff8ab8ac11
--- /dev/null
+++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DBLTextDocumentServiceWorkspaceTest.java
@@ -0,0 +1,77 @@
+/*
+ * DBeaver - Universal Database Manager
+ * Copyright (C) 2010-2025 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.model.lsp;
+
+import org.eclipse.lsp4j.DidOpenTextDocumentParams;
+import org.eclipse.lsp4j.TextDocumentItem;
+import org.jkiss.dbeaver.model.DBConstants;
+import org.jkiss.dbeaver.model.app.DBPProject;
+import org.jkiss.dbeaver.model.app.DBPWorkspace;
+import org.jkiss.dbeaver.model.lsp.DBLServerSessionProvider;
+import org.jkiss.dbeaver.model.lsp.DBLTextDocumentService;
+import org.jkiss.dbeaver.model.lsp.context.ContextAwareDocument;
+import org.jkiss.dbeaver.registry.DataSourceRegistry;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+
+public class DBLTextDocumentServiceWorkspaceTest extends H2DataSourceTest {
+ private static final String PROJECT_ID = "DBLTextDocumentServiceProject";
+ private static final String DATA_SOURCE_ID = "workspace-test-data-source";
+
+ private DBLTextDocumentService service = new DBLTextDocumentService(new TestSessionProvider());
+
+ protected DBPWorkspace workspace;
+ protected DBPProject project;
+
+ @Before
+ public void setUpWorkspace() {
+ workspace = Mockito.mock(DBPWorkspace.class);
+ project = Mockito.mock(DBPProject.class);
+ DataSourceRegistry registry = Mockito.mock(DataSourceRegistry.class);
+ dataSourceDescriptor.setId(DATA_SOURCE_ID);
+
+ Mockito.when(workspace.getProject(PROJECT_ID)).thenReturn(project);
+ Mockito.when(project.getDataSourceRegistry()).thenReturn(registry);
+ Mockito.when(registry.getDataSource(dataSourceDescriptor.getId()))
+ .thenReturn(dataSourceDescriptor);
+
+ DBLServerSessionProvider sessionProvider = new TestSessionProvider(workspace);
+ service = new DBLTextDocumentService(sessionProvider);
+ }
+
+ @Test
+ public void shouldInitContextWithCustomWorkspace() {
+ Mockito.when(project.getResourceProperty(
+ DocumentServiceTestUtils.BASIC_RESOURCE_PATH,
+ DBConstants.PROP_RESOURCE_DEFAULT_DATASOURCE
+ )).thenReturn(DATA_SOURCE_ID);
+
+ String uri = String.format("lsp://%s/%s", PROJECT_ID, DocumentServiceTestUtils.BASIC_RESOURCE_PATH);
+ TextDocumentItem document = new TextDocumentItem(
+ uri, DocumentServiceTestUtils.SQL_LANGUAGE_ID, 0, "select * from table"
+ );
+
+ service.didOpen(new DidOpenTextDocumentParams(document));
+
+ ContextAwareDocument contextAwareDocument = DocumentServiceTestUtils.getDocument(service, document.getUri());
+ Assert.assertNotNull(contextAwareDocument);
+ Assert.assertEquals(dataSourceDescriptor.getDataSource(), contextAwareDocument.getDataSource());
+ }
+}
diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DocumentServiceTestUtils.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DocumentServiceTestUtils.java
new file mode 100644
index 00000000000..1b62138bd91
--- /dev/null
+++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/DocumentServiceTestUtils.java
@@ -0,0 +1,125 @@
+/*
+ * DBeaver - Universal Database Manager
+ * Copyright (C) 2010-2025 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.model.lsp;
+
+import io.cloudbeaver.model.config.CBAppConfig;
+import io.cloudbeaver.server.CBApplication;
+import org.eclipse.lsp4j.*;
+import org.jkiss.code.NotNull;
+import org.jkiss.code.Nullable;
+import org.jkiss.dbeaver.DBException;
+import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
+import org.jkiss.dbeaver.model.connection.DBPDriver;
+import org.jkiss.dbeaver.model.lsp.DBLTextDocumentService;
+import org.jkiss.dbeaver.model.lsp.context.ContextAwareDocument;
+import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
+import org.jkiss.dbeaver.registry.DataSourceDescriptor;
+import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
+import org.jkiss.dbeaver.runtime.DBWorkbench;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+
+public class DocumentServiceTestUtils {
+
+ private static final String H2_DRIVER_ID = "h2_embedded_v2";
+ public static final String BASIC_RESOURCE_PATH = "scripts/basic.sql";
+ public static final String BASIC_URI = "lsp://project-id/" + BASIC_RESOURCE_PATH;
+ public static final String SQL_LANGUAGE_ID = "SQL";
+
+ @Nullable
+ public static ContextAwareDocument getDocument(@NotNull DBLTextDocumentService service, @NotNull String uri) {
+ try {
+ Field documentsField = service.getClass().getDeclaredField("documentCache");
+ documentsField.setAccessible(true);
+ Map documents =
+ (Map) documentsField.get(service);
+ ContextAwareDocument document = documents.get(uri);
+ documentsField.setAccessible(false);
+ return document;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @NotNull
+ public static TextDocumentItem createQueryDocument(@NotNull String text) {
+ return new TextDocumentItem(BASIC_URI, SQL_LANGUAGE_ID, 0, text);
+ }
+
+ @NotNull
+ public static ContextAwareDocument createAndSaveDocument(
+ @NotNull DBLTextDocumentService service,
+ @NotNull String text,
+ @NotNull String projectId,
+ @NotNull String resourcePath
+ ) {
+ String uri = String.format("lsp://%s/%s", projectId, resourcePath);
+ TextDocumentItem document = new TextDocumentItem(uri, SQL_LANGUAGE_ID, 0, text);
+ service.didOpen(new DidOpenTextDocumentParams(document));
+ return Objects.requireNonNull(getDocument(service, uri));
+ }
+
+ @NotNull
+ public static DocumentFormattingParams setupDocumentAndBuildFormattingParams(
+ @NotNull DBLTextDocumentService service,
+ @NotNull String query
+ ) {
+ TextDocumentItem textDocument = createQueryDocument(query);
+ service.didOpen(new DidOpenTextDocumentParams(textDocument));
+ DocumentFormattingParams formattingParams = new DocumentFormattingParams();
+ formattingParams.setTextDocument(new TextDocumentIdentifier(BASIC_URI));
+ FormattingOptions formattingOptions = new FormattingOptions();
+ formattingParams.setOptions(formattingOptions);
+ return formattingParams;
+ }
+
+ @NotNull
+ public static DataSourceDescriptor createDataSource(
+ @NotNull DBRProgressMonitor monitor
+ ) throws DBException {
+ final DBPDriver driver = DataSourceProviderRegistry.getInstance().findDriver(H2_DRIVER_ID);
+ if (driver == null) {
+ throw new DBException("Could not find H2 driver: " + H2_DRIVER_ID);
+ }
+
+ CBAppConfig config = CBApplication.getInstance().getAppConfiguration();
+ String[] disabledDrivers = Arrays.stream(config.getDisabledDrivers())
+ .filter(driverId -> !Objects.equals(driverId, driver.getFullId()))
+ .toArray(String[]::new);
+ config.setDisabledDrivers(disabledDrivers);
+ config.setEnabledDrivers(new String[] {driver.getFullId()});
+
+ final DBPConnectionConfiguration configuration = new DBPConnectionConfiguration();
+ configuration.setUrl("jdbc:h2:mem:");
+
+ final DataSourceDescriptor dataSourceDescriptor = new DataSourceDescriptor(
+ Objects.requireNonNull(DBWorkbench.getPlatform().getWorkspace().getActiveProject()).getDataSourceRegistry(),
+ DataSourceDescriptor.generateNewId(driver),
+ driver,
+ configuration
+ );
+ dataSourceDescriptor.setName("Test DB");
+ dataSourceDescriptor.setSavePassword(true);
+ dataSourceDescriptor.setTemporary(true);
+ dataSourceDescriptor.connect(monitor, true, true);
+
+ return dataSourceDescriptor;
+ }
+}
diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/H2DataSourceTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/H2DataSourceTest.java
new file mode 100644
index 00000000000..8d8bcc7b34e
--- /dev/null
+++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/H2DataSourceTest.java
@@ -0,0 +1,79 @@
+/*
+ * DBeaver - Universal Database Manager
+ * Copyright (C) 2010-2025 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.model.lsp;
+
+import io.cloudbeaver.CloudbeaverMockTest;
+import org.jkiss.dbeaver.DBException;
+import org.jkiss.dbeaver.ModelPreferences;
+import org.jkiss.dbeaver.model.DBConstants;
+import org.jkiss.dbeaver.model.DBUtils;
+import org.jkiss.dbeaver.model.app.DBPProject;
+import org.jkiss.dbeaver.model.exec.jdbc.JDBCSession;
+import org.jkiss.dbeaver.model.exec.jdbc.JDBCStatement;
+import org.jkiss.dbeaver.model.lsp.DBLTextDocumentService;
+import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
+import org.jkiss.dbeaver.model.runtime.LoggingProgressMonitor;
+import org.jkiss.dbeaver.registry.DataSourceDescriptor;
+import org.jkiss.dbeaver.runtime.DBWorkbench;
+import org.jkiss.dbeaver.utils.PrefUtils;
+import org.junit.Assert;
+import org.junit.Before;
+
+import java.nio.file.Path;
+import java.sql.SQLException;
+
+public abstract class H2DataSourceTest extends CloudbeaverMockTest {
+
+ protected DataSourceDescriptor dataSourceDescriptor;
+ protected DBPProject project;
+ protected JDBCSession databaseSession;
+ protected final DBRProgressMonitor monitor = new LoggingProgressMonitor();
+
+ protected DBLTextDocumentService service;
+
+ @Before
+ public void setUp() throws DBException {
+ PrefUtils.setDefaultPreferenceValue(
+ DBWorkbench.getPlatform().getPreferenceStore(),
+ ModelPreferences.UI_DRIVERS_HOME,
+ Path.of("../../../dbeaver-resources-drivers-jdbc/binaries")
+ );
+
+ dataSourceDescriptor = DocumentServiceTestUtils.createDataSource(monitor);
+ databaseSession = DBUtils.openUtilSession(monitor, dataSourceDescriptor, "Internal test session");
+ project = DBWorkbench.getPlatform().getWorkspace().getProjects().getFirst();
+ project.getDataSourceRegistry().addDataSource(dataSourceDescriptor);
+ project.setResourceProperty(
+ DocumentServiceTestUtils.BASIC_RESOURCE_PATH,
+ DBConstants.PROP_RESOURCE_DEFAULT_DATASOURCE,
+ dataSourceDescriptor.getId()
+ );
+
+ try (JDBCStatement stmt = databaseSession.createStatement()) {
+ Assert.assertFalse(stmt.execute("CREATE TABLE TEST_TABLE1 (id IDENTITY NOT NULL PRIMARY KEY, a VARCHAR, b INT)"));
+ Assert.assertFalse(stmt.execute("CREATE TABLE TEST_TABLE2 (id IDENTITY NOT NULL PRIMARY KEY, a VARCHAR, b INT)"));
+ for (int i = 0; i < 100; i++) {
+ Assert.assertFalse(stmt.execute("INSERT INTO TEST_TABLE1 (a, b) VALUES ('test" + i + "', " + i + ")"));
+ Assert.assertFalse(stmt.execute("INSERT INTO TEST_TABLE2 (a, b) VALUES ('test" + i + "', " + i + ")"));
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException(e);
+ }
+
+ service = new DBLTextDocumentService(new TestSessionProvider());
+ }
+}
diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/TestSessionProvider.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/TestSessionProvider.java
new file mode 100644
index 00000000000..32f7d993247
--- /dev/null
+++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/lsp/TestSessionProvider.java
@@ -0,0 +1,48 @@
+/*
+ * DBeaver - Universal Database Manager
+ * Copyright (C) 2010-2025 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.model.lsp;
+
+import org.jkiss.code.NotNull;
+import org.jkiss.code.Nullable;
+import org.jkiss.dbeaver.model.app.DBPWorkspace;
+import org.jkiss.dbeaver.model.auth.impl.AbstractSessionPersistent;
+import org.jkiss.dbeaver.model.lsp.DBLServerSessionProvider;
+import org.jkiss.dbeaver.runtime.DBWorkbench;
+
+class TestSessionProvider implements DBLServerSessionProvider {
+ private final DBPWorkspace workspace;
+
+ public TestSessionProvider() {
+ this.workspace = DBWorkbench.getPlatform().getWorkspace();
+ }
+
+ public TestSessionProvider(@NotNull DBPWorkspace workspace) {
+ this.workspace = workspace;
+ }
+
+ @Nullable
+ @Override
+ public AbstractSessionPersistent getSession() {
+ return null;
+ }
+
+ @NotNull
+ @Override
+ public DBPWorkspace getWorkspace() {
+ return workspace;
+ }
+}