Intellij plugin
Intellij plugin

Для пользователей explain.tensor.ru - нашего сервиса визуализации PostgreSQL-планов, мы создали плагин "Explain PostgreSQL" для всех IDE от JetBrains, теперь есть возможность форматировать запросы и анализировать планы непосредственно в IDE.

Как использовать плагин и детали о его разработке читайте ниже.


Установка плагина

К сожалению, попытка опубликовать плагин в JetBrains Marketplace завершилась отказом:

На основании имеющейся у нас информации, Вы являетесь лицом, связанным с организацией, находящейся в Российской Федерации, и/или с Правительством Российской Федерации.

По этой причине, а также в связи с недавно опубликованной поправкой к санкционному законодательству ЕС (8-й пакет санкций ЕС), JetBrains не может предоставлять Вам услуги ИТ‑консалтинга, включая техническую поддержку или помощь. Проверки и публикация плагинов на Маркетплейсе JetBrains попадают под определение предоставления технической поддержки.

Поэтому установку плагина надо делать из локального файла - скачиваем его с сайта explain.tensor.ru (раздел Download / Plugins ) и в Settings > Plugins выбираем Install plugin from Disk .

Подключение к БД

создаем новый datasource:

создаем новый datasource
создаем новый datasource

скачиваем JDBC-драйвера PostgreSQL и подключаемся к БД:

подключаемся к БД
подключаемся к БД

Форматирование запроса

в консоли набираем текст запроса и нажимаем Ctrl-Q F или в контекстном меню SQL Format:

форматируем запрос
форматируем запрос

Анализ запроса

В контекстном меню выбираем Explain Plan > Explain Analyze (Tensor) , при этом выполнится запрос EXPLAIN (ANALYZE, BUFFERS)

анализ плана запроса
анализ плана запроса

полученный план отправится в сервис explain.tensor.ru через публичное API , результат откроется в новом окне:

визуализация плана запроса
визуализация плана запроса

Для IDE без плагина Database Tools

В WebStorm для работы с БД требуется установка плагина Database Tools and SQL for WebStorm . Если этот плагин не установлен или у вас community-версия IDE то открыть сайт explain.tensor.ru можно прямо в IDE:

explain.tensor.ru в IDE
explain.tensor.ru в IDE

Настройка плагина

Для тех кто развернул сервис локально, используя вариант self hosted, или использует сайт explain-postgresql.com , можно поменять сайт в настройках IDE в Tools > Explain PostgreSQL:

настройка плагина
настройка плагина

О разработке плагина для JetBrains IDE

создание нового проекта
создание нового проекта

Создаем новый проект "IDE plugin".
В окне есть ссылка на официальную документацию IntelliJ Platform SDK , в ней приведены основные сведения, используемые при разработке.

Создание Tool Window

  1. создаем класс, реализующий интерфейс ToolWindowFactory и переопределяем метод createToolWindowContent , который будет вызываться при нажатии на кнопку окна - здесь в окне создается новая вкладка с сайтом explain.tensor.ru и кнопка новой вкладки:

public class ExplainToolWindowFactory implements ToolWindowFactory, DumbAware {

	@Override
	public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
		createNewTab(project);
		ButtonNewTab newTabBtn = new ButtonNewTab("New Tab", null, AllIcons.Toolbar.AddSlot);
		((ToolWindowEx) toolWindow).setTabActions(newTabBtn);
	}

    public static void createNewTab(Project project) {
		ApplicationManager.getApplication().invokeLater((new Runnable() {
			@Override
			public void run() {
				ExplainBrowser browser = new ExplainBrowser(true);
				ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(explainWindow);
				Content content = ContentFactory.getInstance().createContent(browser.getComponent(), "Explain", false);
				content.setDisposer(browser);
				@NotNull ContentManager contentManager = toolWindow.getContentManager();
				contentManager.addContent(content);
				contentManager.setSelectedContent(content);
			}
		}));
	}
}
  1. кнопка создания новой вкладки - наследуем AnActionButton и переопределяем методы:

    actionPerformed - выполняется при нажатии на кнопку, здесь мы создаем новую вкладку или открываем новое окно браузера если нажали вместе с Ctrl

    updateButton - выполняется для определения доступности кнопки, в нашем случае если JDK с поддержкой JCEF браузера и открыто tool window

public class ButtonNewTab extends AnActionButton {

	public ButtonNewTab(String text, String descr, Icon icon) {
		super(text, descr, icon);
	}

	@Override
	public void actionPerformed(@NotNull AnActionEvent e) {
		if (e.getInputEvent().isControlDown()) {
			BrowserUtil.browse(AppSettingsState.getInstance().getExplainUrl());
		} else {
			ExplainToolWindowFactory.createNewTab(e.getProject());
		}
	}

	@Override
	public void updateButton(@NotNull AnActionEvent e) {
		super.updateButton(e);
		boolean enabled = ExplainBrowser.isSupported && ExplainToolWindowFactory.getToolWindow(e.getProject()).isVisible();
		e.getPresentation().setEnabled(enabled);
	}
}
  1. окно с JCEF-браузером

    JCEF - это реализация Chromium Embedded Framework в Java, определяем возможность его использования методом isSupported и если нет, то вместо компонента с окном браузера выдаем сообщение:

public class ExplainBrowser implements Disposable {
	public static final String EXPLAIN_URL = "https://explain.tensor.ru";
	public static final boolean isSupported = JBCefApp.isSupported();
	private JBCefBrowser browser = null;
	private JPanel panel = null;
	public ExplainBrowser(boolean loadOnStart) {
		if (isSupported) {
			browser = new JBCefBrowser(EXPLAIN_URL);
		} else {
			panel = new JPanel(new BorderLayout());
			JLabel label = new JLabel("JCEF browser is not supported");
			panel.add(label, BorderLayout.CENTER);
		}
	}

	public JComponent getComponent() {
		if (browser == null) {
			return panel;
		} else {
			return browser.getComponent();
		}
	}
}
  1. добавляем описание tool window в plugin.xml в разделе extensions, в параметре factoryClass указываем созданный в п.1 класс. IDE при старте прочитает plugin.xml и создаст наше окно снизу на панели инструментов

  <extensions defaultExtensionNs="com.intellij">
    <toolWindow factoryClass="com.mgorkov.toolwindow.ExplainToolWindowFactory"
                id="Explain PostgreSQL"
                anchor="bottom"
                secondary="true"
                canCloseContents="true"
                largeStripeAnchor="true"
                icon="AllIcons.Xml.Browsers.Chrome"
                doNotActivateOnStart="true"
    />
  </extensions>

Форматирование запроса

Создаем класс SQLFormatter и наследуем от AnAction , в нем также как и в AnActionButton надо переопределить методы actionPerformed и update.

В actionPerformed мы получаем текст документа и, используя API explain.tensor.ru , форматируем и заменяем исходный текст.

public class SQLFormatter extends AnAction {

    private Document document = null;
    private String formatted = null;

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
        Editor editor = e.getData(CommonDataKeys.EDITOR);
        if (editor != null) {
            document = editor.getDocument();
            String text = document.getText();
            formatted = explainApi.beautifier(e, text).join();
            if (formatted != null) {
                WriteCommandAction.runWriteCommandAction(e.getProject(), () -> {
                  document.deleteString(0, document.getTextLength());
                  document.insertString(0, formatted);
                });
            }
        }
    }
}

Чтобы IDE узнала об этом действии нужно зарегистрировать его в plugin.xml в разделе actions , указав реализующий класс, место размещения и комбинацию клавиш. Здесь мы размещаем действие в EditorPopupMenu и оно доступно при нажатии Ctrl Q F

<actions>
  <action id="SQLFormat" class="com.mgorkov.actions.SQLFormatter" text="SQL Format">
    <add-to-group group-id="EditorPopupMenu" anchor="last"/>
    <keyboard-shortcut keymap="$default" first-keystroke="ctrl Q" second-keystroke="F"/>
  </action>
</actions>

При наличии синтаксических ошибок выдается предупреждение:

error hint
error hint

Это реализовано в методе showErrorMessage сервиса ExplainApiService:

private void showErrorMessage(@NotNull AnActionEvent e, String msg) {
    ApplicationManager.getApplication().invokeLater(() -> {
        HintManager.getInstance().showErrorHint(Objects.requireNonNull(e.getData(CommonDataKeys.EDITOR)), msg);
    });
}

Анализ запроса

Для выполнение запросов к базе используем плагин JetBrains Database Tools, он встроен во все IDE , кроме community edition и WebStorm (можно установить отдельно).

Здесь мы наследуем класс ExplainActionBase.Ui и в нем переопределяем метод explainStatement , в котором выполняем запрос и выводим окно браузера с результатом анализа в новой вкладке редактора:

FileEditor[] editors = FileEditorManager.getInstance(console.getProject()).openFile(file, true);
Arrays.stream(editors).filter(e -> e instanceof PgPlanEditor).forEach(e -> {
    PgPlanEditor editor = (PgPlanEditor) e;
    editor.getExplainWindow().updatePlan(mdl.getJson());
});

Далее регистрируем новые действия в контекстном меню JDBC-консоли. Чтобы плагин мог работать в IDE без Database Tools добавим в plugin.xml опциональную зависимость от него:

<depends optional="true" config-file="plugin-withdatabase.xml">com.intellij.database</depends>

и создадим plugin-withdatabase.xml :

<actions>
    <action id="com.mgorkov.actions.ExplainAnalyzeAction"
            class="com.mgorkov.actions.ExplainAnalyzeAction" text="Explain Analyze (Tensor)"
            icon="DatabaseIcons.ConsoleShowPlan">
        <add-to-group group-id="Console.Jdbc.ExplainGroup" anchor="last"/>
    </action>
    <action id="com.mgorkov.actions.ExplainAction" class="com.mgorkov.actions.ExplainAction"
            text="Explain Plan (Tensor)"
            icon="DatabaseIcons.ConsoleShowPlan">
        <add-to-group group-id="Console.Jdbc.ExplainGroup" anchor="last"/>
    </action>
</actions>

Настройка плагина

В IDE есть понятие сервисов, это компоненты плагина, которые загружаются по требованию при вызове getService() . При этом сервисы реализуют паттерн singleton, т.е. при вызове getService() всегда возвращается один и тот же компонент. Хранение настроек плагина реализуется сервисом типа applicationService (всего их три).

Реализация настроек выполнена по схеме MVC.

Роль модели выполняет класс AppSettingsState, реализующий PersistentStateComponent, при этом в аннотации надо указать имя параметра и файла, в котором сохраняются настройки:

@State(
        name = "com.mgorkov.settings.AppSettingsState",
        storages = @Storage("ExplainPostgreSQLSettingsPlugin.xml")
)
public final class AppSettingsState implements PersistentStateComponent<AppSettingsState> {

    private String ExplainUrl = "https://explain.tensor.ru";

    public static AppSettingsState getInstance() {
        return ApplicationManager.getApplication().getService(AppSettingsState.class);
    }

    public void setExplainUrl(String explainUrl) {
        ExplainUrl = explainUrl;
    }
}

Роль контроллера - класс AppSettingsConfigurable реализующий интерфейс Configurable , в котором переопределены методы:

getDisplayName - выдает название пункта меню Settings, используется при поиске

public class AppSettingsConfigurable implements Configurable {

    private AppSettingsComponent appSettingsComponent;

    @Override
    public @NlsContexts.ConfigurableName String getDisplayName() {
        return "Explain PostgreSQL";
    }

    @Override
    public @Nullable JComponent createComponent() {
        appSettingsComponent = new AppSettingsComponent();
        return appSettingsComponent.getPanel();
    }

    @Override
    public void apply() throws ConfigurationException {
        AppSettingsState settings = AppSettingsState.getInstance();
        String explainUrl = appSettingsComponent.getExplainUrl();
        try {
            new URL(explainUrl).openStream().close();
            settings.setExplainUrl(explainUrl);
        } catch (Exception e) {
            throw new ConfigurationException(e.getMessage());
        }
    }
}
настройка плагина
настройка плагина

apply - выполняет проверку на валидность URL и возможность подключиться к нему, в случае ошибок бросает исключение ConfigurationException , текст из которого выводится снизу на странице настроек.

createComponent - создает страницу настроек, о ней ниже.

Роль представления реализует класс AppSettingsComponent , в котором создается форма с текстовым полем и меткой:

public class AppSettingsComponent {
    private final JPanel panel;
    private final JBTextField explainUrlTextField = new JBTextField();

    public AppSettingsComponent() {
        panel = FormBuilder.createFormBuilder()
                .addLabeledComponent(new JBLabel("Explain PostgreSQL site: "), explainUrlTextField, 1, false)
                .addComponentFillVertically(new JPanel(), 0)
                .getPanel();
    }

    public JPanel getPanel() {
        return panel;
    }

    public String getExplainUrl() {
        return explainUrlTextField.getText();
    }

    public void setExplainUrl(@NotNull String url) {
        explainUrlTextField.setText(url);
    }
}

Регистрируем модель и контроллер в plugin.xml , для контроллера необходимо указать пункт меню Settings, в котором будут наши настройки, т.е. tools:

<extensions defaultExtensionNs="com.intellij">
  <applicationConfigurable parentId="tools"
                           instance="com.mgorkov.settings.AppSettingsConfigurable"
                           id="com.mgorkov.settings.AppSettingsConfigurable"
                           displayName="Explain PostgreSQL"
  />
  <applicationService serviceImplementation="com.mgorkov.settings.AppSettingsState"/>
</extensions>

Отладка плагина

Для вывода сообщений в лог используем Logger :

private static final Logger log = Logger.getInstance(ExplainApiService.class);
log.debug("POST JSON: " + jsonObject);

Лог хранится в файле idea.log , открыть который можно через меню Help -> Open Log in Editor

По умолчанию в лог пишутся все события уровня INFO.

Чтобы записать события с уровнем DEBUG надо добавить список имен в Help -> Diagnostic Tools -> Debug Log Settings :

настройка debug
настройка debug

Для включения внутренних команд в IDE надо добавить опцию idea.is.internal=true в Help -> Edit Custom Properties

Настройка DevTools
Настройка DevTools
DevTools menu
DevTools menu

После перезапуска появится дополнительный пункт меню Tools -> Internal Actions

просмотр элемента
просмотр элемента

Теперь при одновременном нажатии Ctrl-Alt-A и щелчке мыши на любом элементе можно посмотреть его состав, как в DevTools браузера

Сборка и публикация плагина

Сборка осуществляется Gradle, при скачивании зависимостей в защищенной корпоративной среде могут быть ошибки типа "not found", для решения надо добавить сертификат в хранилище, используя стандартный пароль "changeit", и перезапустить IDE:

keytool -import -trustcacerts -alias root -file <файл с сертификатом> -keystore <путь к IDE>/jbr/lib/security/cacerts

Конфигурация сборки задается в файле build.gradle.kts , при необходимости можно поменять параметры по умолчанию. Например для варианта сборки с библиотекой json версии 20231013 и тестового запуска плагина в IDE IntelliJ IDEA Ultimate Edition версии 2022.2.5 :

dependencies {
  implementation("org.json:json:20231013")
}

intellij {
  version.set("2022.2.5")
  type.set("IU") // Target IDE Platform

  plugins.set(listOf("com.intellij.database"))
}

Описание всех опций сборки можно почитать здесь.

Gradle создает набор задач, для которых можно поменять конфигурацию запуска, например чтобы при тестовом запуске IDE лог-файл idea.log отображался в консоли в отдельной вкладке надо поменять конфигурацию задачи runIde и добавить idea.log , путь к которому можно посмотреть в Help -> Show Log in Files:

меняем конфигурацию задачи runIde
меняем конфигурацию задачи runIde

Для подписи плагина потребуется поменять конфигурацию задачи signPlugin и добавить несколько переменных окружения:

настраиваем signPlugin
настраиваем signPlugin

Эти переменные используются при сборке и уже прописаны в build.gradle.kts :

signPlugin {
    certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
    privateKey.set(System.getenv("PRIVATE_KEY"))
    password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}

Процесс создания подписей есть в документации, но там нет описания для получения значений в виде одной строки, а это можно сделать так:

# строка для переменной PRIVATE_KEY
openssl genpkey -aes-256-cbc -algorithm RSA -pkeyopt rsa_keygen_bits:4096 | openssl rsa | tee private.pem | openssl base64 | tr -d '\n'

# строка для CERTIFICATE_CHAIN
openssl req -key private.pem -new -x509 -days 365 | openssl base64 | tr -d '\n'

Для проверки совместимости плагина с разными версиями версиями IDE можно использовать задачу verifyPlugin и добавить параметры для нее в конфиг сборки (build.gradle.kts) , например мы хотим проверить плагин в DataGrip версий 2023.3 и 3.1 :

runPluginVerifier {
  ideVersions.set(listOf(
          "DG-2023.3.1",
          "DG-2023.3"
  ))
}

По умолчанию верификация плагина происходит на сайте plugins.jetbrains.com при загрузке новой версии плагина, на каких версиях производится проверка задается в build.gradle.kts :

patchPluginXml {
  sinceBuild.set("222")
  untilBuild.set("233.*")
}

Можно также запустить проверку на сайте для конкретной версии, например для версий 221.* плагин не совместим из-за отсутствия методов:

проверка совместимости на сайте
проверка совместимости на сайте

Для публикации плагина первичная загрузка производится вручную, собранный файл находится в build/distributions (его кстати можно установить прямо с диска, для этого зайти в Settings -> Plugins -> Install Plugin from Disk)

В дальнейшем можно автоматизировать публикацию, для этого на сайте plugins.jetbrains.com в своем профиле во вкладке Tokens сгенерить токен и прописать его в переменной PUBLISH_TOKEN в конфигурации задачи publishToken.


В статье написал обо всех этапах создания плагина, пропустив детали реализации, не относящиеся непосредственно к теме разработки плагинов в IDE JetBrains.

Код плагина опубликован в github под лицензией MIT.

Комментарии (17)


  1. Shiftuia
    15.01.2024 13:36
    +1

    Не знаю какое review JetBrains делает для плагинов, но я бы в любом случае по дефолту не доверял плагину, скачанному не из официального стора.


    1. ris58h
      15.01.2024 13:36
      +1

      Open source же. Бери да собирай.


      1. Shiftuia
        15.01.2024 13:36

        Согласен. Я и написал, что ПО, не собранное тобой, а скачанное из неизвестного источника, не заслуживает доверия.


  1. 1nd1go
    15.01.2024 13:36

    Спасибо за описание внутренностей плагина, как раз было интересно как можно самому сделать, а документация что-то у них не сильно наглядная


    1. ris58h
      15.01.2024 13:36

      Что именно показалось затруднительным в документации?


      1. 1nd1go
        15.01.2024 13:36
        +1

        Ну она больше справочник, а хотелось бы "вот hello-world плагин, он устроен вот так, его можно расширять вот так", потому что я далек от написания таких компонентов, и по справочнику понять общую концепцию разработки сложно.


        1. ris58h
          15.01.2024 13:36

          Там есть пошаговый туториал создания языкового плагина с нуля.


  1. Apokalepsis
    15.01.2024 13:36

    А что сервис делает сверх стандартных explain analyze?


    1. Kilor
      15.01.2024 13:36
      +1

      В базовой версии: корректно вычисляет потребление ресурсов на каждом узле с учетом вложенности, подсказывает, как можно устранить неэффективность в запросе, какие индексы стоит добавить в БД и наглядно это все показывает.

      В расширенной версии добавляется возможность массового сквозного анализа всего лога сервера PostgreSQL, в том числе в онлайн-режиме.


  1. otchgol
    15.01.2024 13:36
    -2

    Всегда было интересно как находятся ресурсы на написание статей, но не находятся на поддержание плагина. Актуальная версия ide не принимает его.


    1. MGorkov Автор
      15.01.2024 13:36

      Какая у Вас ide и версия?


      1. Obake
        15.01.2024 13:36

        тоже ловлю ошибку на DataGrip 2023.3.2 (macOS):
        java.lang.NoClassDefFoundError: org/json/JSONObject
        at com.mgorkov.api.ExplainApiService.beautifier(ExplainApiService.java:47)


        1. MGorkov Автор
          15.01.2024 13:36

          Проверяли на macOS Ventura 13.3.1 и DataGrip 2023.3.2 , ошибок нет.

          На какой версии macOS запускаете?


          1. Obake
            15.01.2024 13:36

            M1 - macOS Sonoma Version 14.2.1 (23C71)


            1. MGorkov Автор
              15.01.2024 13:36

              На нашем M1 Sonoma все работает, https://youtu.be/_OH91vkcBZY

              Возможно, конфликтует с другими плагинами. Можете прислать список установленных плагинов на почту me.gorkov@tensor.ru ?


  1. boraldomaster
    15.01.2024 13:36
    -1

    Я правильно понял, что для анализа запроса он отправляется на какой-то сторонний сервер, ещё и где-то в России? Тогда сразу масса вопросов.

    1. Как можно сделать анализ запроса не имея под рукой схему базы данных?

    2. Как можно сделать анализ запроса не имея под рукой самих данных (ведь использование индексов сильно зависит от количества строк в таблице, например иногда выгоднее делать Full Scan)?

    Сливается ли это всё на российский сервер?
    А если нет, точность таких анализов и сопутствующих "советов по оптимизации" вероятно мизерная.

    В идее есть стандартный встроенный плагин для построения плана запроса (он даже на ваших скриншотах виден). Насколько есть смысл рисковать словом данных непонятно куда вместо использования этого плагина?


    1. Kilor
      15.01.2024 13:36

      Давайте отвечу я, поскольку вопросы больше касаются сервиса, нежели плагина.

      1. Если вас напрягает "сервер где-то в России", то можете настроить отправку из плагина на зеркало explain-postgresql.com "где-то в Amazon" или развернуть self-hosted версию где-то на собственных мощностях и отправлять туда.

      2. Схема БД несложно восстанавливается на основании данных из плана, но она нужна только для принципиальных изменений алгоритма работы запроса, как вот тут, например.

      3. Большинство проблем производительности запроса легко определяется на основании одного лишь плана, и решения там - типовые, в том числе и насчет индексов.