Compare commits

...
Author SHA1 Message Date
b021ceec4b dbeaver/pro#9665 adds global network profiles (#4465)
* dbeaver/pro#9665 adds global network profiles

* dbeaver/pro#9665 marks global profiles in selector

* dbeaver/pro#9665 cleanup

* dbeaver/pro#9665 fixes document-global G color so it adapts to the theme

* dbeaver/pro#9665 splits logic to tab service & bootstrap

* dbeaver/pro#9665 hides connections tab if no sub-tabs was registered

* Merge branch 'devel' into 9665-cb-global-network-profiles-in-cloudbeaver

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2026-07-28 19:52:23 +03:00
Ruslan Musaevandsergeyteleshev d318579745 dbeaver/pro#9755 navigator settings sync (#4491)
* dbeaver/pro#9755 navigator settings sync

* dbeaver/pro#9755 navigator settings sync

* dbeaver/pro#9755 navigator settings sync

* dbeaver/pro#9755 adds synchronization for changing connection settings

---------

Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-07-28 19:25:16 +03:00
Ainur 8d17ed0ecd dbeaver/pro#9964 fix generating urls for saml with custom root uri (#4486) 2026-07-28 19:02:00 +03:00
DaniilandDaria Marutkina 3199a99b29 dbeaver/pro#9822 API for full DDL for PostgreSQL (#4477)
* dbeaver/pro#9822 API for full DDL for PostgreSQL

* dbeaver/pro#9822 linter fix

* dbeaver/pro#9822 linter fix

* dbeaver/pro#9822 add cancellable async tasks for DDL generation

* dbeaver/pro#9822 linter fix

* dbeaver/pro#9822 linter fix

* dbeaver/pro#9822 comment fix

* dbeaver/pro#9822 comment fix

* dbeaver/pro#9822 remove PostgreSQL-only restriction for full DDL feature

* dbeaver/pro#9822 review fix: add deprecated annotations to ghl schemas

* dbeaver/pro#9822 review fix: revert changes for metadataGetNodeDDL

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-28 16:42:41 +02:00
Sychev AndreyandDaria Marutkina 39008f7170 dbeaver/pro#9865 feat: add title prop to SelectField component (#4501)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-28 16:42:29 +02:00
8bf33d539f dbeaver/pro#9905 adds fallback to get connection from nodeId in case nodePath is not set (#4499)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-07-28 16:37:56 +02:00
Elizabeth 42f6720acd dbeaver/pro#10033 Fix file connections creation (#4503) 2026-07-28 16:08:27 +02:00
sergeyteleshevandEvgenia 0a3cb12c0b dbeaver/pro#9798 adds sorting for columns in user tables (#4484)
* dbeaver/pro#9798 adds sorting for columns in user tables

* dbeaver/pro#9798 adds comparator helpers

* dbeaver/pro#9798 moves user helpers to compareUser file

* dbeaver/pro#9798 fixes naming and any

* dbeaver/pro# adds grantedUsersIdsMap to make less computations

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-28 13:22:43 +02:00
202823e731 dbeaver/pro#9719 move ai to community (#4471)
* dbeaver/pro#9719 move ai to community

* dbeaver/pro#9719 return prompt generator id input and fix system message type

* dbeaver/pro#9719 return prompt generator id for conversation

* dbeaver/pro#9719 remove rag

* dbeaver/pro#9719 move ai to ce

* dbeaver/pro#9719 fix description for ai chat

* dbeaver/pro#9719 clear feature

* dbeaver/pro#9719 fix conflicts

* dbeaver/pro#9719 remove unused import

---------

Co-authored-by: alex <48489896+devnaumov@users.noreply.github.com>
Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-07-28 13:15:33 +02:00
Ruslan MusaevandAinur ed088fe923 dbeaver/pro#9934 api for user list downloaded (#4498)
Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com>
2026-07-28 12:01:49 +02:00
df21c3396a dbeaver/pro#9617 api for data-import settings (#4423)
* dbeaver/pro#9617 api for data import settings

* dbeaver/pro#9617 api for data import settings

* dbeaver/pro#9617 api for data import settings

* dbeaver/pro#9617 api for data import settings

* dbeaver/pro#9617 api for data import settings

* dbeaver/pro#9617 api for data import settings

* dbeaver/pro#9617 feat: support new import API on FE

* dbeaver/pro#9617 api for data import settings

---------

Co-authored-by: Sychev Andrey <nukemoore@gmail.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com>
2026-07-28 12:01:24 +02:00
90ccdc6432 dbeaver/pro#9621 feat: add state to diagram tab (#4495)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-07-28 11:39:37 +02:00
sergeyteleshevandmr-anton-t dba371a320 dbeaver/pro#10011 fixes js-yaml, brace-expansion, postcss CVEs (#4500)
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-07-28 11:19:12 +02:00
d96cd8f0d2 dbeaver/pro#9912 add ability to save null for default auto commit mode (#4481)
* dbeaver/pro#9912 add ability to save null for default auto commit mode

* dbeaver/pro#9912 return null value by default

* dbeaver/pro#9912 return valid values

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2026-07-28 11:09:37 +02:00
ElizabethandDiana bd6e7d48f3 dbeaver/pro#9491 Handle exception from setUrl (#4479)
Co-authored-by: Diana <31996417+uslss@users.noreply.github.com>
2026-07-28 07:51:45 +02:00
8b41e4530f dbeaver/pro#4490 bump postgres version (#4493)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-27 18:51:50 +02:00
alexandDaria Marutkina a555f1e759 dbeaver/pro#4425 add controlled mode (#4468)
* dbeaver/pro#4425 add controlled mode

* dbeaver/pro#4425 update yarn lock

* dbeaver/pro#4425 use controlled state for users

* dbeaver/pro#4425 update copyright

* dbeaver/pro#4425 update notice

* dbeaver/pro#4425 update tokens

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-27 17:09:40 +02:00
alexandDaria Marutkina fdaa40e431 dbeaver/pro#8663 add auto resize textarea (#4497)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-27 17:02:18 +02:00
sergeyteleshevandDaria Marutkina 4c2f9d92e5 dbeaver/pro#9170 adds download button for the script (#4474)
* dbeaver/pro#9170 adds download button for the script

* dbeaver/pro#9170 gets sql filename from node or model

* dbeaver/pro#9170 adds action buttons

* dbeaver/pro#9170 adds borders to the buttons

* dbeaver/pro#9170 fixes build

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-27 14:35:17 +02:00
sergeyteleshevandmr-anton-t a92011eee6 dbeaver/pro#9967 fixes immutable & fast-uri CVEs (#4492)
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-07-27 11:52:01 +02:00
ElizabethandDiana 61a7757a0e dbeaver/pro#9445 Update Clickhouse driver version (#4480)
Co-authored-by: Diana <31996417+uslss@users.noreply.github.com>
2026-07-24 13:11:02 +02:00
Nikita Akilov a53ca3592f dbeaver/dbeaver-devops#2751 stop including curl with cloud products (#4494) 2026-07-24 11:22:42 +02:00
Sychev AndreyandEvgenia 9e682617e6 dbeaver/pro#9638 [CB] refresh is needed to see replaced connectionfolder in another instance (#4464)
* dbeaver/pro#9638 fix: update project filtering logic to use project IDs for active projects

Changed all four spots from reference equality to id comparison, matching the rest of the codebase, see https://github.com/dbeaver/cloudbeaver/issues/4430

* dbeaver/pro#9638 refactor(connectionUpdateHandler): move node to a new parent on change

* dbeaver/pro#9638 revert commit eb2ba7871e

* dbeaver/pro#9638 fix: update node parents on move in other session

Server events don't carry the actual data, so besides outdating (which lets the handlers invalidate the previous state, e.g. the old location in the navigation tree) we explicitly load the connection to guarantee that the handlers receive the actual state

* dbeaver/pro#9638 fix: sync connection folder move from another session

* dbeaver/pro#9638 fix: revert back connectionUpdateHandler

* dbeaver/pro#9638 fix: remove try catch

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-23 17:33:26 +02:00
sergeyteleshevandDaria Marutkina 16dda810d7 dbeaver/pro#9171 adds ability to open sql editor with context from Generate SQL dialog (#4488)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-23 16:30:47 +02:00
Sychev AndreyandEvgenia 6042f16342 dbeaver/pro#9426 feat: add loading state to save button (#4487)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-23 16:30:31 +02:00
Viktor Kirst 0afdd06c80 dbeaver/pro#9768 Fix long-polling session cookie after session ID rotation (#4467) 2026-07-23 16:04:31 +03:00
Sychev Andreyandmr-anton-t 3158aebca4 dbeaver/pro#9877 UI fixes (#4473)
* dbeaver/pro#9877 fix: select render field popover

* dbeaver/pro#9877 fix: revert back ripple hover effect

* dbeaver/pro#9877 fix: make all outlines 1px by default

* dbeaver/pro#9877 fix: switch outline

* dbeaver/pro#9877 fix: topbar buttons outline

* dbeaver/pro#9877 fix: tablist styles

* dbeaver/pro#9877 fix: rule priorities

* dbeaver/pro#9877 fix: eslint

* dbeaver/pro#9877 fix: eslint

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-07-23 13:07:10 +02:00
mayerro 99866fdc12 Revert "dbeaver/dbeaver-devops#2751 remove curl from base java to fix cve (#4483)"
This reverts commit f54f377344.
2026-07-23 11:17:11 +02:00
sergeyteleshev a58eb59a32 dbeaver/pro#9938 fixes 8 CVEs (#4478) 2026-07-23 11:08:30 +02:00
DaniilandEvgenia eda1f8720f dbeaver/pro#9721 [CB] Add a flag into reverse proxy allowing new user… (#4460)
* dbeaver/pro#9721 [CB] Add a flag into reverse proxy allowing new users creation

* dbeaver/pro#9721 [CB] code style fix

* dbeaver/pro#9721 [CB] code style fix

* dbeaver/pro#9721 move the method isAutoUserProvisioningEnabled to SMAuthProviderExternal

* dbeaver/pro#9721 linter fix

* dbeaver/pro#9721 code review fix

* dbeaver/pro#9721 code review fix

* dbeaver/pro#9721 code review fix

* dbeaver/pro#9721 Fix login for existing users without reverse proxy credentials when auto user provisioning is disabled

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-23 10:09:30 +02:00
Nikolay f54f377344 dbeaver/dbeaver-devops#2751 remove curl from base java to fix cve (#4483)
* dbeaver/dbeaver-devops#2751 removed curl from base java to fix cve

* dbeaver/dbeaver-devops#2751 removed an extra comment
2026-07-22 16:31:49 +03:00
alex 81a9be17b8 dbeaver/pro#9659 add dnd scroll container (#4476) 2026-07-22 14:24:53 +02:00
alex af42d7f50a dbeaver/pro#9910 check key also (#4469) 2026-07-20 15:55:01 +02:00
Daria Marutkina d083ac1330 dbeaver/pro#9879 Update README file (#4472)
Added changelog for version 26.1.3, including updates on data editor, general improvements, data transfer enhancements, accessibility features, security fixes, and database driver updates.
2026-07-20 13:21:53 +02:00
Serge Rider cdb450dfde dbeaver/pro#7329 Navigator view actions model refactoring (get rid of UI).
Git service and navigator web service code style.
2026-07-15 12:01:30 +02:00
dbeaver-devops d0d9ee19ab dbeaver/pro#9879 26.1.4 version bump 2026-07-14 19:24:11 +02:00
9327ce0ac2 dbeaver/pro#9522 integrate profile selector to connection form (#4440)
* dbeaver/pro#9522 integrate profile selector to connection form

* dbeaver/pro#9522 handles edge cases

* dbeaver/pro#9522 uses rest instead of default getter

* dbeaver/pro#9522 adds preprocessing to handle null/undefined differences arrived from backend for both: connection form network handlers & profile form network handlers

* dbeaver/pro#9522 eslint fix

* dbeaver/pro#9522 refactors profiles

* dbeaver/pro#9522 removes preprocess

* dbeaver/pro#9522 adds header & footer items

* dbeaver/pro#9522 truncates items values

* dbeaver/pro#9522 fix retrieving creds from profiles

* dbeaver/pro#9522 fix test temporary connection

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2026-07-14 18:50:59 +02:00
1fb9dd7b5a dbeaver/pro#9723 Fix UI styles for improved accessibility and consistency (#4459)
* dbeaver/pro#9723 feat: add focus-visible styles for improved accessibility

* dbeaver/pro#9723 fix: adjust gutter size in ComboboxPopover for consistent spacing

* dbeaver/pro#9723 fix: update button outline offset for consistency

* dbeaver/pro#9723 fix: update hover opacity

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-07-14 18:37:55 +02:00
alexandDaria Marutkina e82cac479b dbeaver/pro#9861 pass null as valid filter (#4461)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-14 17:44:07 +02:00
bf6afb0340 dbeaver/pro#9712 Databend driver updated to 0.4.8 (#4453)
Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-14 16:13:26 +02:00
4779c3b405 dbeaver/pro#9000 adds API token settings (#4455)
* dbeaver/pro#9000 adds API token settings

* dbeaver/pro#9000 adds save extend instead

* dbeaver/pro#9000 adds observable to the schema

* dbeaver/pro#9000 adds LooseObject in favor of server config part state extension

---------

Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-14 12:15:43 +02:00
31ee01b6fa dbeaver/pro#9614 add predefined param for connection type (#4454)
* dbeaver/pro#9614 add predefined param for connection type

* dbeaver/pro#9614 add connection type selector

* dbeaver/pro#9614 make menu indicator more visible

* dbeaver/pro#9614 add extra colors

* dbeaver/pro#9614 outdate connections

* dbeaver/pro#9614 load types in bootstrap phrase

* dbeaver/pro#9614 always use useConnectionTypeColor

* dbeaver/pro#9614 remove unused import

* dbeaver/pro#9614 add useConnectionTypeLoader hook

* dbeaver/pro#9614 add default hint

* dbeaver/pro#9614 do not wrap arg:item by default

* dbeaver/pro#9614 use predefined unset for all types

* dbeaver/pro#9614 use common mapped unset color

---------

Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-13 22:11:10 +02:00
025944a867 dbeaver/pro#8947 user provisioning return user ids (#4429)
Co-authored-by: Sychev Andrey <44414066+SychevAndrey@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-07-13 16:00:32 +02:00
Serge Rider caee9914b8 dbeaver/pro#9851 Refactor data transfer producer. Smart mapping bug fix. (#4458) 2026-07-13 15:43:53 +02:00
sergeyteleshevandEvgenia 6e2b1b7a17 dbeaver/pro#8849 fixes spacing for bind parameters popup (#4456)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-09 19:22:41 +02:00
sergeyteleshevandDaria Marutkina a379c2d7ba dbeaver/pro#9755 adds frontend cleanup for js artifacts before each app build (#4450)
* dbeaver/pro#9755 adds frontend cleanup for js artifacts before each app build

* dbeaver/pro#9775 adds x to log (revert it)

* dbeaver/pro#9775 adds clear commands to build frontend script

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-08 17:44:33 +02:00
sergeyteleshevandDaria Marutkina 8b1054a148 dbeaver/pro#9775 adds unit test for getLocalizedDisplayName (#4451)
* dbeaver/pro#9775 adds unit test for getLocalizedDisplayName

* dbeaver/pro#9775 adds unit test for core utils

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-08 17:44:21 +02:00
Ruslan MusaevandEvgenia 7fdc8f7e61 dbeaver/pro#9714 fix ldap injection (#4441)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-08 10:21:15 +02:00
Sychev AndreyandEvgenia 32bebd4bad dbeaver/pro#9303 feat: update edge color variable in CSS for execution plan (#4387)
* dbeaver/pro#9303 feat: update edge color variable in CSS for execution plan

* dbeaver/pro#9303 feat: update node cost bar background and add theme-light styles for edges and captions

* dbeaver/pro#9303 feat: update edge color in execution plan CSS

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-07-08 09:47:46 +02:00
Serge RiderandAinur 4221ea7fe0 dbeaver/pro#8757 Test connection permissions required (#4434)
* dbeaver/pro#8757 Test connection permissions required (the same as createConnection)

* dbeaver/pro#8757 Anno added

* dbeaver/pro#8757 restrict private project use for gql

* dbeaver/pro#8757 change error message

---------

Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
2026-07-07 14:10:26 +02:00
sergeyteleshevandDaria Marutkina af11aaa8fe dbeaver/cloudbeaver#3815 upgrades lib to version where it works (#4165)
* dbeaver/cloudbeaver#3815 upgrades lib to version where it works

* dbeaver/pro#3815 updates react-hotkeys-hook lib

* dbeaver/pro#3815 fixes build

* dbeaver/pro#3815 fixes case dependant hotkeys activation

* dbeaver/pro#3815 allows textbox form tag to support sql editor

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-07-07 14:09:27 +02:00
5c58343a10 dbeaver/pro#8402 feat: enable checkbox selection via keyboard in Team… (#4445)
* dbeaver/pro#8402 feat: enable checkbox selection via keyboard in Teams and Users tables

* dbeaver/pro#8402 feat: enable checkbox selection via keyboard and improve accessibility in tables

fix checkbox and header cell keyboard events

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-07-07 10:59:16 +02:00
Serge Rider a565beb777 dbeaver/pro#9764 Fix launch parameters for Java 25 (#4448) 2026-07-06 16:59:22 +02:00
9df99fb62e dbeaver/pro#8427 feat: enable default fold keymap in CodeMirror (#4446)
* dbeaver/pro#8427 feat: enable default fold keymap in CodeMirror

* dbeaver/pro#8427 feat: add SQL editor shortcuts for folding and unfolding code blocks

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-07-06 14:31:26 +02:00
Serge RiderandMashaKorax b3127b9aa7 dbeaver/pro#9682 AI config profiles (#4437)
Co-authored-by: MashaKorax <84867187+MashaKorax@users.noreply.github.com>
2026-07-06 13:49:35 +02:00
Daria Marutkina f8677a0689 dbeaver/pro#9745 Update README file (#4447)
Updated changelog for version 26.1.2, detailing new features, bug fixes
2026-07-06 11:32:53 +02:00
Ruslan Musaev f22a936507 dbeaver/pro#9749 fix sso login npe (#4443)
* dbeaver/pro#9749 fix sso login npe

* dbeaver/pro#9749 fix sso login npe
2026-07-01 20:05:26 +02:00
Serge Rider b5858637d6 dbeaver/pro#9222 Annotations and permissions 2026-07-01 09:48:04 +02:00
serge-rider 475c8129fc dbeaver/pro#4320 Annotations 2026-06-30 21:55:37 +02:00
dbeaver-devops 50d846d087 dbeaver/pro#9745 26.1.3 version bump 2026-06-30 19:42:04 +02:00
Ainur 16bd3ca575 dbeaver/pro#9612 api for connection types (#4426)
* dbeaver/pro#9612 api for connection types

* dbeaver/pro#9612 add more fields for connection type info

* dbeaver/pro#9612 renaming and event handling

* dbeaver/pro#9612 fix doc and add all inputs
2026-06-30 19:15:46 +02:00
Alexander SkoblikovandMashaKorax 927184e009 dbeaver/pro#3833 fix bug with "null" origin header (#4395)
Co-authored-by: MashaKorax <84867187+MashaKorax@users.noreply.github.com>
2026-06-30 18:27:18 +02:00
d4d9081936 dbeaver/pro#9652 refactor ref api (#4412)
* dbeaver/pro#9652 refactor ref api

* dbeaver/pro#9652 refactor ref api

* dbeaver/pro#9652 refactor ref api

* dbeaver/pro#9652 refactor ref api

* dbeaver/pro#9652 remove references

* dbeaver/pro#9652 add anyConstraint

* dbeaver/pro#9652 add references panel

* dbeaver/pro#9652 remove logs

* dbeaver/pro#9652 fix attr position

* dbeaver/pro#9652 fix review

* dbeaver/pro#9652 change naming

* dbeaver/pro#9652 fallback to custom if composite and more than 1 row

* dbeaver/pro#9652 add indicator

* dbeaver/pro#9652 show proper error

* dbeaver/pro#9652 fix review, add targetColumnIndex

* dbeaver/pro#9652 fix targetColumnIndex

* dbeaver/pro#9652 pass target index

* dbeaver/pro#9652 add icons

* dbeaver/pro#9652 check prev constaints, decrease blicking

* dbeaver/pro#9652 fix constraint overwriting

* dbeaver/pro#9652 pass anyConstraint flag to export

* dbeaver/pro#9652 disable references for editors and viewers

* dbeaver/pro#9652 update icons

* dbeaver/pro#9652 show placeholder instead of loader

* dbeaver/pro#9652 show combobox

---------

Co-authored-by: alex <48489896+devnaumov@users.noreply.github.com>
Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-06-30 17:57:40 +02:00
a0bcfba73d dbeaver/pro#9639 load driver properties fix (#4420)
* dbeaver/pro#9639 load driver properties fix

* dbeaver/pro#9639 pass connection config to get driver properties

* dbeaver/pro#9639 execute format action before loading properties

* dbeaver/pro#9639 do not reset properties if tab is selected

* dbeaver/pro#9639 fix import

---------

Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-06-30 17:38:36 +02:00
Sychev Andreyandmr-anton-t 09f4828006 dbeaver/pro#9704 [CB] Fixes outlines for several components (#4431)
* dbeaver/pro#9704 fix: adjust combobox and select control outline properties

* dbeaver/pro#9704 fix: add control outline color and width for checkbox and radio components

* dbeaver/pro#9704 fix: set outline color and width for disclosure component

* dbeaver/pro#9704 refactor: add text overflow handling for combobox item values

remove tw:truncate because it adds hidden text-sm font-size

* dbeaver/pro#9704 fix: update combobox popover and item classes

* dbeaver/pro#9704 fix: correct portal handling in SelectPopover component

* dbeaver/pro#9704 fix: ensure correct portal handling in ComboboxPopover component

* dbeaver/pro#9704 refactor: rename ComboboxType to IComboboxType for consistency

* dbeaver/pro#9704 fix: line-height for combobox items

* dbeaver/pro#9704 style: remove unnecessary letter-spacing from combobox item

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-06-30 17:17:54 +02:00
alexandDaria Marutkina cde64e255b dbeaver/pro#9586 adjust validation (#4427)
* dbeaver/pro#9586 let editing of loaded truncated values

* Revert "dbeaver/pro#9586 let editing of loaded truncated values"

This reverts commit 6b412ef57c.

* dbeaver/pro#9586 adjust quota validation check

* dbeaver/pro#9586 show limit amount

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-06-30 16:42:01 +02:00
7b134cef81 dbeaver/pro#9526 adds profile form (#4390)
* dbeaver/pro#9526 adds profile form base functionality

* dbeaver/pro#9526 adds profiles form

* dbeaver/pro#9526 moves SSHForm to connection-network-handlers to reuse it for admin settings in future without extra deps

* dbeaver/pro#9526 moves ssh files to connection-network-handlers-plugin

* pr fixes

* moves enable form switch inside SSHForm

* dbeaver/pro#9526 shares the rest logic for configuring the ssh with profiles form as well

* dbeaver/pro#9526 cleanup

* dbeaver/pro#9526 adds go back to profiles arrow action

* dbeaver/pro#9526 small ui adjustments

* dbeaver/pro#9618 pr fixes

* dbeaver/pro#9526 creates ProjectInfoOptionsPanelService to incapsulate opening logic with tabId

* dbeaver/pro#9526 adds core-network-handlers plugin

* dbeaver/pro#9526 moves ssh network handler layout to core-network-handlers conig + adds TODO to register it to connection form

* dbeaver/pro#9526 moves registration of connection SSH network handler to plugin-connection-network-handler (from plugin-connection)

* dbeaver/pro#9526 moves registration of connection SSL network handler to plugin-connection-network-handler (from plugin-connection)

* dbeaver/pro#9526 migrates localizations among the plugins + adds missing translations

* dbeaver/pro#9526 cleanup

* dbeaver/pro#9526 fixes build

* dbeaver/pro#9526 core-network-handlers -> plugin-network-handlers

* dbeaver/pro#9526 removes license from locale files

* dbeaver/pro#9526 CONNECTION_NETWORK_HANDLER_SCHEMA -> NETWORK_HANDLER_SCHEMA

* dbeaver/pro#9526 saveChanges interface update

* dbeaver/pro#9526 refactors core -> plugin tokens

* dbeaver/pro#9526 applies copilot suggestions

* dbeaver/pro#9526 pr fixes

* dbeaver/pro#9526 moves test tunnel functionality inside ssh form

* dbeaver/pro#9526 handle network profiles changes

* dbeaver/pro#9526 cleanup

* dbeaver/pro#9526 fixes after review

* dbeaver/pro#9526 fix sending event for profiles

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Ainur <ainur.iagudin@dbeaver.com>
2026-06-30 16:41:10 +02:00
Ruslan Musaev fde7502df8 dbeaver/pro#9713 fix event filter for rotating session (#4436) 2026-06-30 12:36:31 +02:00
Sychev Andrey 955b28c520 dbeaver/pro#9699 fix: reorder items in sql toolbar (#4433) 2026-06-30 12:29:58 +02:00
Ainur 5cfc37e26f dbeaver/pro#9481 save api token max duration fix (#4435) 2026-06-30 12:28:24 +02:00
Ruslan Musaevandsergeyteleshev 42cb6b3a9d dbeaver/pro#9022 fix error file saving (#4424)
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
2026-06-26 12:16:44 +02:00
Serge Riderandkseniaguzeeva ad245e9f4c dbeaver/pro#9700 Save RM datasources correctly (#4425)
* dbeaver/pro#9700 Save RM datasources correctly

* dbeaver/pro#9700 Fix empty collection parameters pass over remote client

---------

Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
2026-06-25 14:35:26 +02:00
08260e6a55 dbeaver/cloudbeaver#4249 added session rotation (#4274) (#4331)
* dbeaver/cloudbeaver#4249 added session rotation (#4274)

* dbeaver/cloudbeaver#4249 added session rotation

* dbeaver/pro#4249 update web session id

* dbeaver/pro#4249 remove log

* dbeaver/pro#4249 remove unused params

* dbeaver/pro#4249 revert non-related changes

* dbeaver/cloudbeaver#4249 rotate session before login

* dbeaver/cloudbeaver#4249 fix code style

* dbeaver/cloudbeaver#4249 rotate only for anon users

* dbeaver/cloudbeaver#4249 added notnulls

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>

* dbeaver/pro#4249 fix websocket for session rotation

---------

Co-authored-by: Dmitrii Barnukov <29237913+Nexus6v2@users.noreply.github.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Ruslan <pupkinvasya7000@gmail.com>
2026-06-25 07:06:38 +02:00
Serge Rider aea12e9fed dbeaver/pro#9700 Save RM datasources correctly 2026-06-24 17:48:29 +02:00
Ruslan MusaevandDaria Marutkina caef729a8d dbeaver/pro#9599 fix allows unauthorized server file stream (#4411)
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
2026-06-24 15:29:07 +02:00
12030822ea dbeaver/pro#9502 [CB] move chart settings export dialogs to the widget (#4399)
* dbeaver/pro#9502 feat: allow importing css files from core-blocks package

Plugins that build UI directly on ui-kit components need the CloudBeaver theme css for these components without importing the components themselves. Expose css files through package exports so a plugin can load them as a side effect.

* dbeaver/pro#9502 fix: update focus-visible styles for select component

* dbeaver/pro#9502 feat: add tokens for combobox items

* dbeaver/pro#9502 feat: load Select/Combobox base styles from core-blocks

ui-kit controls reuse these styles, but the CE wrapper components are lazy, so the CSS only shipped in the lazy chunk, we need those styles for widgets that use UI-kit controls

* dbeaver/pro#9502 feat: enhance Select component with value display styling

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-06-24 12:54:46 +02:00
Mikhailov GrigoriiandSerge Rider 97cf62d333 dbeaver/dbeaver-devops#2660 Added permissions read on contents pull-requests (#4422)
Co-authored-by: Serge Rider <serge@jkiss.org>
2026-06-24 05:01:41 +02:00
8240c92247 dbeaver/pro#8796 [CB] create a separate plugin for the execution plan (#4406)
* dbeaver/pro#8796 refactor: extract Execution Plan into separate plugin

Move all Execution Plan logic out of plugin-sql-editor into a new plugin-sql-editor-execution-plan. plugin-sql-editor no longer knows about execution plan and instead exposes generic extension points

Execution plan tab data now lives in-memory in SqlExecutionPlanService instead of the persisted editor state schema (these tabs are session-only).

* dbeaver/pro#8796 feat: add naming convention rule for exported interfaces

* dbeaver/pro#8796 fix: update js-helpers dependency version in yarn.lock

* dbeaver/pro#8796 refactor: reorder register method for better readability

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-06-23 21:51:29 +02:00
sergeyteleshevandEvgenia 62a194c1b0 dbeaver/pro#7510 adds last login info to users tables (#4421)
* dbeaver/pro#7510 adds last login info to users tables

* dbeaver/pro#7510 shows only date

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-06-23 21:46:51 +02:00
sergeyteleshevandEvgenia 36e954c440 dbeaver/pro#9664 removes undici CVE (#4415)
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-06-23 13:05:38 +02:00
Sychev AndreyandEvgenia a674c183d5 dbeaver/pro#5015 [CB] Ability to use keyboard to enabledisable checkboxes in the properties tab (#4416)
* dbeaver/pro#5015 feat: enable keyboard navigation for context menu in ObjectMenuCell

* dbeaver/pro#5015 feat: enable keyboard activation for context menu in ObjectMenuCell

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
2026-06-23 12:51:04 +02:00
Sychev Andrey 4bab1025d7 dbeaver/pro#9660 fix: selection for search panel inputs (#4418) 2026-06-23 12:49:27 +02:00
Serge Riderandmr-anton-t 7d21186158 dbeaver/pro#9266 Refactor workspace path init and change (#4409)
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-06-22 16:54:26 +02:00
eb5f89dae3 dbeaver/pro#9644 Data filters thread safety (#4407)
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Diana <31996417+uslss@users.noreply.github.com>
2026-06-22 16:06:37 +02:00
70292cb9c0 dbeaver/pro#9588 remove top margin (#4405)
* dbeaver/pro#9588 remove top margin

* dbeaver/pro#9588 fix: move margin from indicator to checkbox itself

add multiline checkbox story for alignment with text

* Revert "dbeaver/pro#9588 remove top margin"

This reverts commit 0b25ed6f00.

---------

Co-authored-by: Sychev Andrey <nukemoore@gmail.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
2026-06-22 14:38:20 +02:00
Evgenia 950f45833a dbeaver/pro#9630 Update README file CB 26.1.1 (#4414)
Added changelog for version 26.1.1
2026-06-22 12:39:55 +02:00
778 changed files with 19185 additions and 2282 deletions
+7 -4
View File
@@ -30,10 +30,13 @@ jobs:
uses: ./.github/workflows/frontend-build.yml
secrets: inherit
# lint-server:
# name: Server
# uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel
# secrets: inherit
lint-server:
name: Server
uses: dbeaver/dbeaver-common/.github/workflows/java-checkstyle.yml@devel
secrets: inherit
permissions:
contents: read
pull-requests: read
lint-frontend:
name: Frontend
+1 -1
View File
@@ -48,6 +48,7 @@
"-Declipse.pde.launch=true",
"-XstartOnFirstThread",
"-Dfile.encoding=UTF-8",
"--enable-native-access=ALL-UNNAMED",
"--add-modules=ALL-DEFAULT",
"--add-opens=java.base/java.io=ALL-UNNAMED",
"--add-opens=java.base/java.lang=ALL-UNNAMED",
@@ -64,7 +65,6 @@
"--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED",
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
"--add-opens=java.base/sun.security.ssl=ALL-UNNAMED",
"--add-opens=java.base/sun.security.action=ALL-UNNAMED",
"--add-opens=java.base/sun.security.util=ALL-UNNAMED",
"--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED",
"--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED",
+46
View File
@@ -28,6 +28,52 @@ You can see a live demo of CloudBeaver server here: https://demo.cloudbeaver.io
## Changelog
### 26.1.3 2026-07-20
- Data Editor:
- Fixed copying of large JSON values in the Data Editor - now the full value can be copied and transferred.
- Updated the Reference panel to show only relevant rows when a foreign key is null.
- General:
- Added connection types to visually distinguish database environments using background colors across the Database Navigator and application UI. Users with edit permissions can assign built-in environment types color-coded configurations.
- Data Transfer:
- Improved memory usage when importing large CSV files.
- Fixed CSV export with multi-character quotes and improved quote/delimiter validation
- Accessibility:
- Enabled the use of the Enter or space keys to toggle checkboxes in the administration tabs and the metadata viewer.
- Added the ability to expand and collapse code block elements, including JSON, using the keyboard.
- Fixed shortcuts behavior for AZERTY keyboards. Now shortcuts like Ctrl/Cmd + Z work correctly.
- Security:
- Resolved an authorization vulnerability when testing connections by enforcing strict permission checks. This mitigation prevented unauthenticated users from exploiting JDBC requests to read arbitrary server files.
- Resolved an LDAP injection vulnerability by properly escaping user input in the authentication username field, preventing attackers from manipulating search filters, bypassing access restrictions, and enumerating directory users.
- Fixed the high vulnerability (GHSA-gv7w-rqvm-qjhr) in the esbuild library. The library was updated to version 0.28.1.
- Fixed the high vulnerability (CVE-2026-12143) in the form-data library. The library was updated to version 4.0.6.
- Databases and drivers:
- The Databend driver was updated to version 0.4.8.
- Fixed an issue related to the display of metadata in the navigation tree for DB2, Firebird, and MySQL.
### 26.1.2 2026-07-06
- Data Editor:
- Added the References panel showing related records from other tables connected by foreign keys. It helps to explore table relationships directly in the Data editor.
- Resolved an issue where the Data Editor ignored the text preview's maximum length quota. Increasing this quota now enables the ability to edit large values.
- General:
- Added the Last Login time to the user form and the user table in the administration part.
- Fixed the bug with the inability to select entered text in the Find and Replace panel in editors.
- Accessibility:
- Added the ability to use the Enter or Space key to turn checkboxes on/off in the connection properties tab.
- Security:
- Fixed CVE-2026-55449. Resolved a local file disclosure vulnerability by enforcing strict validation of file identifiers and path containment during data updates.
- Updated session handling to prevent session fixation vulnerabilities. The application generates new session identifiers upon login.
- Fixed the high vulnerability (CVE-2026-12151) in the undici library. The library was updated to version 6.27.0.
### 26.1.1 2026-06-22
- General: Added a "Project Info" tab containing the project description to the project context menu.
- Security:
- Fixed a cross-site scripting vulnerability caused by unsanitized user input in the web interface.
- Resolved a security flaw related to user-controlled file paths by adding strict validation to prevent unauthorized resource access.
- Fixed the critical vulnerability (CVE-2026-9277) in the shell-quote library (updated to version 1.8.4).
### 26.1.0 2026-06-01
### Changes since 26.0.0:
+9 -1
View File
@@ -8,7 +8,15 @@ mkdir -p ./cloudbeaver/web
cd ../../cloudbeaver/webapp
yarn install --immutable
cd ./packages/product-default
yarn clear
cd common-react
yarn clear
cd ../common-typescript
yarn clear
cd ../packages/product-default
yarn run bundle
if [[ "$?" -ne 0 ]] ; then
-2
View File
@@ -9,8 +9,6 @@ RUN set -eux; \
apt-get update && \
apt-get dist-upgrade -y --no-install-recommends && \
apt-get install -y --no-install-recommends \
# curl required for historical reasons, see https://github.com/adoptium/containers/issues/255
curl \
# java.lang.UnsatisfiedLinkError: libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory
# java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11FontManager
# https://github.com/docker-library/openjdk/pull/235#issuecomment-424466077
+1 -1
View File
@@ -17,6 +17,7 @@ If Not Defined JAVA_OPTS (
java %JAVA_OPTS% ^
-Dfile.encoding=UTF-8 ^
--enable-native-access=ALL-UNNAMED ^
--add-modules=ALL-DEFAULT ^
--add-opens=java.base/java.io=ALL-UNNAMED ^
--add-opens=java.base/java.lang=ALL-UNNAMED ^
@@ -33,7 +34,6 @@ java %JAVA_OPTS% ^
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED ^
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED ^
--add-opens=java.base/sun.security.ssl=ALL-UNNAMED ^
--add-opens=java.base/sun.security.action=ALL-UNNAMED ^
--add-opens=java.base/sun.security.util=ALL-UNNAMED ^
--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED ^
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED ^
+1 -1
View File
@@ -11,6 +11,7 @@ echo "Starting Cloudbeaver Server"
exec java ${JAVA_OPTS} \
-Dfile.encoding=UTF-8 \
--enable-native-access=ALL-UNNAMED \
--add-modules=ALL-DEFAULT \
--add-opens=java.base/java.io=ALL-UNNAMED \
--add-opens=java.base/java.lang=ALL-UNNAMED \
@@ -27,7 +28,6 @@ exec java ${JAVA_OPTS} \
--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED \
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens=java.base/sun.security.ssl=ALL-UNNAMED \
--add-opens=java.base/sun.security.action=ALL-UNNAMED \
--add-opens=java.base/sun.security.util=ALL-UNNAMED \
--add-opens=java.security.jgss/sun.security.jgss=ALL-UNNAMED \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Model
Bundle-SymbolicName: io.cloudbeaver.model.cli;singleton:=true
Bundle-Version: 1.0.13.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 1.0.15.qualifier
Bundle-Release-Date: 20260803
Bundle-RequiredExecutionEnvironment: JavaSE-21
Bundle-ActivationPolicy: lazy
Require-Bundle: org.jkiss.dbeaver.model.cli;visibility:=reexport
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.model.cli</artifactId>
<version>1.0.13-SNAPSHOT</version>
<version>1.0.15-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Model
Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true
Bundle-Version: 1.0.104.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 1.0.106.qualifier
Bundle-Release-Date: 20260803
Bundle-RequiredExecutionEnvironment: JavaSE-21
Bundle-ActivationPolicy: lazy
Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport,
@@ -13,7 +13,7 @@ meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbProductName.
meta.io.cloudbeaver.model.app.ServletSystemInformationCollector.smDbProductVersion.name=Security manager database product version
meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Read-only connection
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Auto-commit
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = Auto-commit mode
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Keep-alive interval (seconds)
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultSchema.name = Default schema
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultCatalog.name = Default catalog
@@ -1,5 +1,5 @@
meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Только для чтения
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Авто-коммит
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = Авто-коммит
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Интервал Keep-alive (в секундах)
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultSchema.name = Схема по умолчанию
meta.io.cloudbeaver.model.WebExpertSettingsProperties.defaultCatalog.name = Каталог по умолчанию
@@ -1,4 +1,4 @@
meta.io.cloudbeaver.model.WebExpertSettingsProperties.readOnly.name = Kết nối chỉ đọc
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = Tự động commit
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = Tự động commit
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = Giữ kết nối (tính bằng giây)
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = Không tự động ngắt kết nối
@@ -1,3 +1,3 @@
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommit.name = 自动提交
meta.io.cloudbeaver.model.WebExpertSettingsProperties.autoCommitMode.name = 自动提交
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.name = 保持连接 (秒)
meta.io.cloudbeaver.model.WebExpertSettingsProperties.keepAliveInterval.description = 无自动断开连接
+1 -1
View File
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.model</artifactId>
<version>1.0.104-SNAPSHOT</version>
<version>1.0.106-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -77,9 +77,7 @@ public class WebConnectionConfigInputHandler<T extends WebConnectionConfig, C ex
}
dataSource.setFolder(input.getFolder() != null ? registry.getFolder(input.getFolder()) : null);
if (input.isDefaultAutoCommit() != null) {
dataSource.getConnectionConfiguration().getBootstrap().setDefaultAutoCommit(input.isDefaultAutoCommit());
}
dataSource.getConnectionConfiguration().getBootstrap().setDefaultAutoCommit(input.isDefaultAutoCommit());
WebDataSourceUtils.setConnectionConfiguration(
dataSource.getDriver(),
dataSource.getConnectionConfiguration(),
@@ -367,6 +367,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
dataSourceRegistry.initializeDataSources();
}
@Nullable
@Override
public DataSourceParseResults loadDataSources(
@NotNull List<DBPDataSourceConfigurationStorage> storages,
@@ -382,6 +383,7 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat
dataSourceRegistry.saveDataSources();
}
@NotNull
@Override
public DataSourceConfigurationManager getConfigurationManager() {
return dataSourceRegistry.getConfigurationManager();
@@ -357,6 +357,13 @@ public class WebSessionProjectImpl extends WebProjectImpl implements DBPAdaptabl
return getInputConfigHandler(configInput).createDataSourceContainer();
}
public void updateDataSourceContainerFromInput(
@NotNull WebConnectionConfig configInput,
@NotNull DataSourceDescriptor dataSource
) throws DBWebException {
getInputConfigHandler(configInput).updateDataSource(dataSource);
}
@NotNull
public WebConnectionConfig getConnectionConfigInput(@Nullable Map<String, Object> configMap) {
return new WebConnectionConfig(configMap == null ? Map.of() : configMap);
@@ -69,6 +69,13 @@ public interface SMAuthProviderExternal<AUTH_SESSION extends SMSession> extends
@NotNull WebUser user,
boolean selfIdentity) throws DBException;
/**
* Whether a user that is not found should be provisioned (auto-created) on external login.
*/
default boolean isAutoUserProvisioningEnabled(@Nullable SMAuthProviderCustomConfiguration providerConfig) {
return true;
}
/**
* Make some post authentication actions
*/
@@ -0,0 +1,62 @@
/*
* 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.model;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.utils.CommonUtils;
public enum AutoCommitMode {
DEFAULT("Default", null),
AUTO_COMMIT("Auto commit", true),
MANUAL_COMMIT("Manual commit", false);
private final String title;
private final Boolean value;
AutoCommitMode(@NotNull String title, @Nullable Boolean value) {
this.title = title;
this.value = value;
}
@NotNull
@Override
public String toString() {
return title;
}
@Nullable
public Boolean getValue() {
return value;
}
@NotNull
public static AutoCommitMode fromValue(@Nullable Boolean value) {
if (value == null) {
return DEFAULT;
}
return value ? AUTO_COMMIT : MANUAL_COMMIT;
}
@Nullable
public static Boolean from(@Nullable String mode) {
if (CommonUtils.isEmpty(mode)) {
return null;
}
return valueOf(mode).getValue();
}
}
@@ -65,6 +65,7 @@ public class WebConnectionConfig {
private Boolean defaultAutoCommit;
private String defaultCatalogName;
private String defaultSchemaName;
private String connectionType;
@NotNull
private Map<String, String> defaultUserPreferences = new LinkedHashMap<>();
@@ -102,8 +103,14 @@ public class WebConnectionConfig {
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_KEEP_ALIVE_INTERVAL, -1);
readOnly = JSONUtils.getBoolean(
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_READ_ONLY);
defaultAutoCommit = JSONUtils.getBoolean(
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_AUTO_COMMIT, true);
if (expertSettingsValues != null) {
defaultAutoCommit = AutoCommitMode.from(
JSONUtils.getString(expertSettingsValues, WebExpertSettingsProperties.PROP_AUTO_COMMIT_MODE));
} else {
defaultAutoCommit = JSONUtils.getBoolean(params, WebExpertSettingsProperties.PROP_AUTO_COMMIT, true);
}
defaultCatalogName = JSONUtils.getString(
expertSettingsValues != null ? expertSettingsValues : params, WebExpertSettingsProperties.PROP_DEFAULT_CATALOG);
defaultSchemaName = JSONUtils.getString(
@@ -120,6 +127,7 @@ public class WebConnectionConfig {
for (Map<String, Object> nhc : JSONUtils.getObjectList(params, "networkHandlersConfig")) {
networkHandlersConfig.add(new WebNetworkHandlerConfigInput(nhc));
}
connectionType = JSONUtils.getString(params, "connectionType");
}
@Property
@@ -381,4 +389,13 @@ public class WebConnectionConfig {
public void setDefaultUserPreferences(@NotNull Map<String, String> defaultUserPreferences) {
this.defaultUserPreferences = defaultUserPreferences;
}
@Nullable
public String getConnectionType() {
return connectionType;
}
public void setConnectionType(@Nullable String connectionType) {
this.connectionType = connectionType;
}
}
@@ -467,7 +467,7 @@ public class WebConnectionInfo {
@Property
public Map<String, Object> getExpertSettingsValues() {
Map<String, Object> expertSettings = new LinkedHashMap<>();
expertSettings.put(WebExpertSettingsProperties.PROP_AUTO_COMMIT, isAutocommit());
expertSettings.put(WebExpertSettingsProperties.PROP_AUTO_COMMIT_MODE, AutoCommitMode.fromValue(isAutocommit()));
expertSettings.put(WebExpertSettingsProperties.PROP_KEEP_ALIVE_INTERVAL, getKeepAliveInterval());
expertSettings.put(WebExpertSettingsProperties.PROP_READ_ONLY, isReadOnly());
expertSettings.put(WebExpertSettingsProperties.PROP_DEFAULT_CATALOG, getDefaultCatalogName());
@@ -541,12 +541,9 @@ public class WebConnectionInfo {
}
@Property
public boolean isAutocommit() {
Boolean isAutoCommit = dataSourceContainer.getConnectionConfiguration().getBootstrap().getDefaultAutoCommit();
if (isAutoCommit == null) {
return true;
}
return isAutoCommit;
@Nullable
public Boolean isAutocommit() {
return dataSourceContainer.getConnectionConfiguration().getBootstrap().getDefaultAutoCommit();
}
@Property
@@ -586,6 +583,11 @@ public class WebConnectionInfo {
return tools;
}
@NotNull
public String getConnectionType() {
return dataSourceContainer.getConnectionConfiguration().getConnectionType().getId();
}
/**
* Updates param that checks whether credentials were saved only in session.
*/
@@ -593,4 +595,13 @@ public class WebConnectionInfo {
this.credentialsSavedInSession = credentialsSavedInSession;
}
/**
* Database-dependent driver configuration used to drive data import
*/
@Property
@NotNull
public WebDriverConfiguration getDriverConfiguration() {
return new WebDriverConfiguration(dataSourceContainer);
}
}
@@ -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.model;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.meta.Property;
/**
* Generic id/name/description dictionary value.
*/
public class WebDictionaryValueInfo {
@NotNull
private final String id;
@NotNull
private final String name;
@Nullable
private final String description;
public WebDictionaryValueInfo(@NotNull String id, @NotNull String name, @Nullable String description) {
this.id = id;
this.name = name;
this.description = description;
}
@Property
@NotNull
public String getId() {
return id;
}
@Property
@NotNull
public String getName() {
return name;
}
@Property
@Nullable
public String getDescription() {
return description;
}
}
@@ -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.model;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.sql.SQLDialectInsertReplaceMethod;
import org.jkiss.dbeaver.model.sql.SQLDialectMetadata;
import org.jkiss.dbeaver.model.struct.DBSDataBulkLoader;
import org.jkiss.utils.CommonUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class WebDriverConfiguration {
@NotNull
private final DBPDataSourceContainer dataSourceContainer;
public WebDriverConfiguration(@NotNull DBPDataSourceContainer dataSourceContainer) {
this.dataSourceContainer = dataSourceContainer;
}
/**
* 'On duplicate key' replace methods supported by this connection's SQL dialect
*/
@Property
@NotNull
public List<WebDictionaryValueInfo> getSupportedInsertReplaceMethods() {
SQLDialectMetadata dialect = dataSourceContainer.getDriver().getScriptDialect();
List<SQLDialectInsertReplaceMethod> replaceMethods = dialect.getSupportedInsertReplaceMethodsDescriptors();
if (CommonUtils.isEmpty(replaceMethods)) {
return Collections.emptyList();
}
return replaceMethods.stream()
.map(method -> new WebDictionaryValueInfo(method.getId(), method.getLabel(), method.getDescription()))
.collect(Collectors.toList());
}
/**
* Whether the connection's database supports bulk load during data import
*/
@Property
public boolean isSupportsBulkLoad() {
DBPDataSource dataSource = dataSourceContainer.getDataSource();
return dataSource != null && DBUtils.getAdapter(DBSDataBulkLoader.class, dataSource) != null;
}
/**
* Whether the connection's database supports transaction
*/
@Property
public boolean isSupportsTransactions() {
DBPDataSource dataSource = dataSourceContainer.getDataSource();
return dataSource != null && dataSource.getInfo().supportsTransactions();
}
}
@@ -1,3 +1,19 @@
/*
* 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.model;
import org.jkiss.code.NotNull;
@@ -5,6 +21,7 @@ import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.DBPObject;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.connection.DBPDriverConstants;
import org.jkiss.dbeaver.model.meta.IPropertyValueListProvider;
import org.jkiss.dbeaver.model.meta.IPropertyValueValidator;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.utils.CommonUtils;
@@ -15,6 +32,7 @@ import org.jkiss.utils.CommonUtils;
public class WebExpertSettingsProperties implements DBPObject {
public static final String PROP_READ_ONLY = "readOnly";
public static final String PROP_AUTO_COMMIT = "autocommit";
public static final String PROP_AUTO_COMMIT_MODE = "autoCommitMode";
public static final String PROP_KEEP_ALIVE_INTERVAL = "keepAliveInterval";
public static final String PROP_DEFAULT_CATALOG = "defaultCatalogName";
public static final String PROP_DEFAULT_SCHEMA = "defaultSchemaName";
@@ -30,9 +48,15 @@ public class WebExpertSettingsProperties implements DBPObject {
return 0;
}
@Property(order = 2, id = PROP_AUTO_COMMIT, visibleIf = AutoCommitFieldValidator.class)
public boolean isAutoCommit() {
return true;
@NotNull
@Property(
order = 2,
id = PROP_AUTO_COMMIT_MODE,
visibleIf = AutoCommitFieldValidator.class,
listProvider = AutoCommitListProvider.class
)
public AutoCommitMode isAutoCommitMode() {
return AutoCommitMode.DEFAULT;
}
@Property(order = 3, id = PROP_READ_ONLY, visibleIf = ReadOnlyFieldValidator.class)
@@ -40,11 +64,13 @@ public class WebExpertSettingsProperties implements DBPObject {
return false;
}
@Nullable
@Property(order = 4, id = PROP_DEFAULT_CATALOG, visibleIf = DefaultCatalogFieldVisibleValidator.class)
public String getDefaultCatalog() {
return null;
}
@Nullable
@Property(order = 5, id = PROP_DEFAULT_SCHEMA, visibleIf = DefaultSchemaFieldVisibleValidator.class)
public String getDefaultSchema() {
return null;
@@ -85,4 +111,18 @@ public class WebExpertSettingsProperties implements DBPObject {
return CommonUtils.toBoolean(object.driver.getDriverParameter(DBPDriverConstants.PARAM_SUPPORTS_SCHEMA_SELECTION), true);
}
}
public static class AutoCommitListProvider implements IPropertyValueListProvider<WebExpertSettingsProperties> {
@Override
public boolean allowCustomValue() {
return false;
}
@Nullable
@Override
public Object[] getPossibleValues(@Nullable WebExpertSettingsProperties object) {
return AutoCommitMode.values();
}
}
}
@@ -37,7 +37,6 @@ import org.jkiss.dbeaver.model.impl.app.BaseApplicationImpl;
import org.jkiss.dbeaver.model.impl.app.BaseWorkspaceImpl;
import org.jkiss.dbeaver.model.rm.RMController;
import org.jkiss.dbeaver.model.secret.DBSSecretController;
import org.jkiss.dbeaver.model.websocket.event.WSEventController;
import org.jkiss.dbeaver.runtime.IVariableResolver;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.dbeaver.utils.RuntimeUtils;
@@ -89,12 +88,6 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
return new SharedFileLockManager(getApplicationInstanceId());
}
@Nullable
@Override
public Path getDefaultWorkingFolder() {
return getServerConfigurationController().getWorkspacePath();
}
@Override
public boolean isHeadlessMode() {
return true;
@@ -192,20 +185,20 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
@SuppressWarnings("unchecked")
public static void patchConfigurationWithProperties(
Map<String, Object> configProps, IVariableResolver varResolver
@NotNull Map<String, Object> configProps,
@NotNull IVariableResolver varResolver
) {
for (Map.Entry<String, Object> entry : configProps.entrySet()) {
Object propValue = entry.getValue();
if (propValue instanceof String) {
entry.setValue(GeneralUtils.replaceVariables((String) propValue, varResolver));
if (propValue instanceof String strValue) {
entry.setValue(GeneralUtils.replaceVariables(strValue, varResolver));
} else if (propValue instanceof Map) {
patchConfigurationWithProperties((Map<String, Object>) propValue, varResolver);
} else if (propValue instanceof List) {
List value = (List) propValue;
} else if (propValue instanceof List value) {
for (int i = 0; i < value.size(); i++) {
Object colItem = value.get(i);
if (colItem instanceof String) {
value.set(i, GeneralUtils.replaceVariables((String) colItem, varResolver));
if (colItem instanceof String strItem) {
value.set(i, GeneralUtils.replaceVariables(strItem, varResolver));
} else if (colItem instanceof Map) {
patchConfigurationWithProperties((Map<String, Object>) colItem, varResolver);
}
@@ -218,6 +211,8 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
@Override
public Object start(IApplicationContext context) {
initializeApplicationServices();
setWorkspacePath(getServerConfigurationController().getWorkspacePath());
try {
startServer();
} catch (Exception e) {
@@ -255,27 +250,17 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
}
@NotNull
@Override
public Path getWorkspaceDirectory() {
return getServerConfigurationController().getWorkspacePath();
}
public String getApplicationId() {
try {
return getApplicationInstanceId();
} catch (DBException e) {
return null;
// Fallback to hash code
return getClass().getSimpleName() + "#" + hashCode();
}
}
@NotNull
@Override
public WSEventController getEventController() {
return null;
}
public abstract ServletServerConfigurationController getServerConfigurationController();
public abstract ServletServerConfigurationController<?> getServerConfigurationController();
@Override
public boolean isEnvironmentVariablesAccessible() {
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -53,9 +53,6 @@ public interface ServletApplication extends DBPApplication {
@NotNull
Path getDataDirectory(boolean create);
@NotNull
Path getWorkspaceDirectory();
@NotNull
Path getHomeDirectory();
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* 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.
@@ -23,6 +23,7 @@ import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
public interface ServletAuthApplication extends ServletApplication {
@NotNull
ServletAuthConfiguration getAuthConfiguration();
@Nullable
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* 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.
@@ -67,8 +67,7 @@ public abstract class BaseLocalResourceController implements RMController {
@Nullable
@Override
public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties)
throws DBException {
public RMProject getProject(@NotNull String projectId, boolean readResources, boolean readProperties) throws DBException {
RMProject project = makeProjectFromId(projectId, true);
if (project == null) {
return null;
@@ -110,8 +109,7 @@ public abstract class BaseLocalResourceController implements RMController {
}
@Override
public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds)
throws DBException {
public String getProjectsDataSources(@NotNull String projectId, @Nullable String[] dataSourceIds) throws DBException {
DBPProject projectMetadata = getWebProject(projectId, false);
return doFileReadOperation(
projectId,
@@ -40,6 +40,7 @@ import org.jkiss.dbeaver.model.fs.lock.LockTarget;
import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl;
import org.jkiss.dbeaver.model.impl.auth.SessionContextImpl;
import org.jkiss.dbeaver.model.navigator.DBNLocalFolder;
import org.jkiss.dbeaver.model.net.DBWNetworkProfile;
import org.jkiss.dbeaver.model.rm.*;
import org.jkiss.dbeaver.model.security.SMAdminController;
import org.jkiss.dbeaver.model.sql.DBQuotaException;
@@ -48,6 +49,7 @@ import org.jkiss.dbeaver.model.websocket.event.WSSessionLogUpdatedEvent;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceEvent;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDatasourceFolderEvent;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSNetworkProfileEvent;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceParseResults;
import org.jkiss.dbeaver.registry.ResourceTypeDescriptor;
@@ -407,8 +409,13 @@ public class LocalResourceController extends BaseLocalResourceController {
registry::createDataSource
)
);
Map<String, DBWNetworkProfile> oldNetworkProfiles = registry.getNetworkProfiles().getProfiles().stream()
.collect(Collectors.toMap(
DBWNetworkProfile::getProfileId,
p -> p
));
DataSourceParseResults parseResults = super.updateProjectDataSourcesConfig(projectId, configuration, dataSourceIds);
sendDataSourcesConfigUpdatedEvent(registry, oldDataSources, parseResults);
sendConfigUpdatedEvent(registry, oldDataSources, oldNetworkProfiles, parseResults);
return parseResults != null;
}
@@ -501,14 +508,48 @@ public class LocalResourceController extends BaseLocalResourceController {
return DBNLocalFolder.makeLocalFolderItemPath(projectId, folderPath);
}
private void sendDataSourcesConfigUpdatedEvent(
private void sendConfigUpdatedEvent(
@NotNull DBPDataSourceRegistry registry,
@NotNull Map<String, DataSourceDescriptor> oldDataSources,
@NotNull Map<String, DBWNetworkProfile> oldNetworkProfiles,
@Nullable DataSourceParseResults parseResults
) {
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null || oldDataSources.isEmpty()) {
if (parseResults == null || credentialsProvider.getActiveUserCredentials() == null) {
return;
}
boolean profilesChanged = !parseResults.removedProfiles.isEmpty();
// We don't need to check for updated profiles if there are already added or removed profiles
if (!profilesChanged) {
for (DBWNetworkProfile updatedProfile : parseResults.updatedProfiles) {
if (oldNetworkProfiles.containsKey(updatedProfile.getProfileId())) {
DBWNetworkProfile oldProfile = oldNetworkProfiles.get(updatedProfile.getProfileId());
if (!oldProfile.equalConfigurations(updatedProfile)) {
profilesChanged = true;
break;
}
} else {
// profile was added
profilesChanged = true;
break;
}
}
}
if (profilesChanged) {
ServletAppUtils.getServletApplication().getEventController().addEvent(
WSNetworkProfileEvent.update(
credentialsProvider.getActiveUserCredentials().getSmSessionId(),
credentialsProvider.getActiveUserCredentials().getUserId(),
registry.getProject().getId()
)
);
}
if (oldDataSources.isEmpty()) {
return;
}
List<String> updatedConfigurationDataSourceIds = new ArrayList<>();
List<String> updatedNameDataSourceIds = new ArrayList<>();
List<String> updatedInternalConfigurationDataSourceIds = new ArrayList<>();
@@ -612,7 +653,6 @@ public class LocalResourceController extends BaseLocalResourceController {
)
);
}
}
@NotNull
@@ -123,6 +123,16 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
}
}
public void migrateEventHandlersTo(@NotNull BaseWebSession target) {
synchronized (sessionEventHandlers) {
for (CBWebSessionEventHandler handler : sessionEventHandlers) {
handler.migrateToSession(target);
target.addEventHandler(handler);
}
sessionEventHandlers.clear();
}
}
public boolean updateSMSession(SMAuthInfo smAuthInfo) throws DBException {
return userContext.refresh(smAuthInfo);
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -68,6 +68,13 @@ public class WebSessionEventsFilter {
}
}
public void migrateTo(@NotNull WebSessionEventsFilter target) {
synchronized (target.subscribedProjectIds) {
target.subscribedEventTopics.addAll(this.subscribedEventTopics);
target.subscribedProjectIds.addAll(this.subscribedProjectIds);
}
}
public boolean isEventAllowed(WSEvent event) {
if (event.isForceProcessed()) {
return true;
@@ -54,7 +54,7 @@ public class ServerGlobalWorkspace extends BaseWorkspaceImpl {
@NotNull DBPPlatform platform,
@NotNull ServletApplication application
) {
super(platform, application.getWorkspaceDirectory());
super(platform, application.getWorkspacePath());
this.application = application;
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* 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.
@@ -30,7 +30,7 @@ public interface DBWServiceBindingServlet<APPLICATION extends ServletApplication
return true;
}
void addServlets(APPLICATION application, DBWServletContext servletContext) throws DBException;
void addServlets(@NotNull APPLICATION application, @NotNull DBWServletContext servletContext) throws DBException;
@NotNull
default List<String> getExcludedServletPaths(@NotNull APPLICATION application) {
@@ -192,13 +192,16 @@ public class ServletAppUtils {
@NotNull String serviceId,
@NotNull String origin
) throws DBException {
String finalOrigin = webAuthApplication.modifyOrigin(origin);
String finalOrigin = ServletAppUtils.substringBeforeRootURI(webAuthApplication.modifyOrigin(origin));
String serviceUriSegment = removeSideSlashes(webAuthApplication.getAuthServiceUriSegment());
String rootUri = removeSideSlashes(getServletApplication().getRootURI());
if (finalOrigin.endsWith("/" + rootUri) && serviceUriSegment.startsWith(rootUri + "/")) {
finalOrigin = finalOrigin.substring(0, finalOrigin.length() - rootUri.length());
}
StringBuilder authUriBuilder = new StringBuilder(removeSideSlashes(finalOrigin));
if (CommonUtils.isNotEmpty(rootUri)) {
authUriBuilder.append("/").append(rootUri);
}
if (CommonUtils.isNotEmpty(serviceUriSegment)) {
authUriBuilder.append("/").append(serviceUriSegment);
}
@@ -297,12 +300,21 @@ public class ServletAppUtils {
}
}
@NotNull
public static String getHostOriginFromRequest(@NotNull HttpServletRequest request) {
return substringBeforeRootURI(getOriginFromRequest(request));
}
@NotNull
public static String getOriginFromRequest(@NotNull HttpServletRequest request) {
String origin = request.getHeader(HEADER_ORIGIN);
if (log.isTraceEnabled()) {
log.trace("Origin header: " + origin);
}
if ("null".equals(origin)) {
//strange identity provider bug
origin = null;
}
if (CommonUtils.isEmpty(origin)) {
origin = request.getHeader(HEADER_X_ORIGIN);
if (log.isTraceEnabled()) {
@@ -33,6 +33,7 @@ import org.jkiss.dbeaver.model.access.DBAAuthCredentials;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials;
@@ -40,6 +41,7 @@ import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
import org.jkiss.dbeaver.model.net.ssh.SSHConstants;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceDisconnectEvent;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
@@ -55,9 +57,12 @@ public class WebDataSourceUtils {
private WebDataSourceUtils() {
}
public static void saveCredentialsInDataSource(WebConnectionInfo webConnectionInfo, DBPDataSourceContainer dataSourceContainer, DBPConnectionConfiguration configuration) {
public static void saveCredentialsInDataSource(
@NotNull WebConnectionInfo webConnectionInfo,
@NotNull DBPDataSourceContainer dataSourceContainer,
@NotNull DBPConnectionConfiguration configuration
) {
// Properties passed from web
// webConnectionInfo may be null in some cases (e.g. connection test when no actual connection exist yet)
Map<String, Object> authProperties = webConnectionInfo.getSavedAuthProperties();
if (authProperties != null) {
authProperties.forEach((s, o) -> configuration.setAuthProperty(s, CommonUtils.toString(o)));
@@ -305,9 +310,7 @@ public class WebDataSourceUtils {
if (config.getKeepAliveInterval() >= 0) {
dsConfig.setKeepAliveInterval(config.getKeepAliveInterval());
}
if (config.isDefaultAutoCommit() != null) {
dsConfig.getBootstrap().setDefaultAutoCommit(config.isDefaultAutoCommit());
}
dsConfig.getBootstrap().setDefaultAutoCommit(config.isDefaultAutoCommit());
dsConfig.getBootstrap().setDefaultCatalogName(config.getDefaultCatalogName());
dsConfig.getBootstrap().setDefaultSchemaName(config.getDefaultSchemaName());
// Save provider props
@@ -320,8 +323,19 @@ public class WebDataSourceUtils {
if (config.getConfigurationType() != null) {
dsConfig.setConfigurationType(config.getConfigurationType());
}
if (config.getConnectionType() != null) {
DBPConnectionType connectionType = DataSourceProviderRegistry.getInstance().getConnectionType(config.getConnectionType(), null);
if (connectionType != null) {
dsConfig.setConnectionType(connectionType);
}
}
if (CommonUtils.isEmpty(config.getUrl())) {
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
try {
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
} catch (DBException e) {
log.error("Error preparing connection URL", e);
}
}
// Save network handlers
if (config.getNetworkHandlersConfig() != null) {
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -16,6 +16,8 @@
*/
package io.cloudbeaver.websocket;
import io.cloudbeaver.model.session.BaseWebSession;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.websocket.event.WSEvent;
@@ -24,4 +26,6 @@ public interface CBWebSessionEventHandler {
void close();
void migrateToSession(@NotNull BaseWebSession newSession);
}
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Community Product
Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true
Bundle-Version: 26.1.2.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 26.1.4.qualifier
Bundle-Release-Date: 20260803
Bundle-RequiredExecutionEnvironment: JavaSE-21
Bundle-ActivationPolicy: lazy
Require-Bundle: io.cloudbeaver.server.ce
@@ -16,6 +16,10 @@
</product>
</extension>
<extension point="io.cloudbeaver.feature">
<feature id="ai" label="AI Integration" description="Enables AI Chat in the application."/>
</extension>
<extension point="org.jkiss.dbeaver.application">
<application
id="cloudbeaver-ce"
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.product.ce</artifactId>
<version>26.1.2-SNAPSHOT</version>
<version>26.1.4-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -2,8 +2,8 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Cloudbeaver Base JDBC drivers
Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true
Bundle-Version: 1.0.149.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 1.0.151.qualifier
Bundle-Release-Date: 20260803
Bundle-Vendor: DBeaver Corp
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: io.cloudbeaver.resources.drivers.base
@@ -9,6 +9,6 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.resources.drivers.base</artifactId>
<version>1.0.149-SNAPSHOT</version>
<version>1.0.151-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: CloudBeaver Community
Bundle-SymbolicName: io.cloudbeaver.server.ce;singleton:=true
Bundle-Version: 26.1.2.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 26.1.4.qualifier
Bundle-Release-Date: 20260803
Bundle-RequiredExecutionEnvironment: JavaSE-21
Bundle-ActivationPolicy: lazy
Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.server.ce</artifactId>
<version>26.1.2-SNAPSHOT</version>
<version>26.1.4-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -14,11 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.admin;
package io.cloudbeaver.model.config;
import io.cloudbeaver.model.config.CBAppConfig;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBConstants;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
@@ -57,7 +57,7 @@ public class AdminServerConfig {
@Nullable
private String bindSessionToIp;
public AdminServerConfig(Map<String, Object> params) {
public AdminServerConfig(@NotNull Map<String, Object> params) {
this.serverName = JSONUtils.getString(params, "serverName");
this.adminName = JSONUtils.getString(params, "adminName");
this.adminPassword = JSONUtils.getString(params, "adminPassword");
@@ -181,6 +181,7 @@ public abstract class CBApplication<T extends CBServerConfig>
return getServerConfigurationController().getServerConfiguration();
}
@NotNull
@Override
public ServletAuthConfiguration getAuthConfiguration() {
return getAppConfiguration();
@@ -228,11 +229,11 @@ public abstract class CBApplication<T extends CBServerConfig>
Location instanceLoc = Platform.getInstanceLocation();
try {
if (!instanceLoc.isSet()) { // always false?
URL wsLocationURL = getWorkspaceDirectory().toUri().toURL();
URL wsLocationURL = getWorkspacePath().toUri().toURL();
instanceLoc.set(wsLocationURL, true);
}
} catch (Exception e) {
log.error("Error setting workspace location to " + getWorkspaceDirectory().toAbsolutePath(), e);
log.error("Error setting workspace location to " + getWorkspacePath().toAbsolutePath(), e);
return;
}
this.systemInformationCollector = createSystemInformationCollector();
@@ -443,7 +444,7 @@ public abstract class CBApplication<T extends CBServerConfig>
@NotNull
public Path getDataDirectory(boolean create) {
Path dataDir = getWorkspaceDirectory().resolve(CBConstants.RUNTIME_DATA_DIR_NAME);
Path dataDir = getWorkspacePath().resolve(CBConstants.RUNTIME_DATA_DIR_NAME);
if (create && !Files.exists(dataDir)) {
try {
Files.createDirectories(dataDir);
@@ -751,6 +752,7 @@ public abstract class CBApplication<T extends CBServerConfig>
eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId, null));
}
@NotNull
@Override
public abstract CBServerConfigurationController<T> getServerConfigurationController();
@@ -93,6 +93,7 @@ public class CBApplicationCE extends CBApplication<CBServerConfig> {
return new LocalFileController(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath().resolve(DBFileController.DATA_FOLDER));
}
@NotNull
@Override
public CBServerConfigurationControllerEmbedded<CBServerConfig> getServerConfigurationController() {
return serverConfigController;
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* 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.
@@ -20,10 +20,7 @@ import com.google.gson.*;
import com.google.gson.reflect.TypeToken;
import io.cloudbeaver.model.app.BaseServerConfigurationController;
import io.cloudbeaver.model.app.BaseServletApplication;
import io.cloudbeaver.model.config.CBAppConfig;
import io.cloudbeaver.model.config.CBServerConfig;
import io.cloudbeaver.model.config.PasswordPolicyConfiguration;
import io.cloudbeaver.model.config.SMControllerConfiguration;
import io.cloudbeaver.model.config.*;
import io.cloudbeaver.utils.ServletAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -374,6 +371,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
try (Writer out = new OutputStreamWriter(Files.newOutputStream(runtimeConfigPath), StandardCharsets.UTF_8)) {
Gson gson = new GsonBuilder()
.setStrictness(Strictness.LENIENT)
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.setPrettyPrinting()
.create();
gson.toJson(configurationProperties, out);
@@ -508,7 +506,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
@NotNull
protected Map<String, Object> collectServerConfigProperties(
@NotNull CBServerConfig serverConfig,
Map<String, Object> originServerConfig
@NotNull Map<String, Object> originServerConfig
) {
var serverConfigProperties = new LinkedHashMap<String, Object>();
if (!CommonUtils.isEmpty(serverConfig.getServerName())) {
@@ -633,6 +631,9 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
return serverConfiguration;
}
@NotNull
public abstract CBServerConfigurationMapper<T, ? extends AdminServerConfig> createServerConfigurationInputMapper();
public CBAppConfig getAppConfiguration() {
return appConfiguration;
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -19,6 +19,7 @@ package io.cloudbeaver.server;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import io.cloudbeaver.model.config.AdminServerConfig;
import io.cloudbeaver.model.config.CBServerConfig;
import io.cloudbeaver.model.config.WebDatabaseConfig;
import org.jkiss.code.NotNull;
@@ -41,6 +42,11 @@ public class CBServerConfigurationControllerEmbedded<T extends CBServerConfig> e
super(serverConfig, homeDirectory);
}
@NotNull
public CBServerConfigurationMapper<T, ? extends AdminServerConfig> createServerConfigurationInputMapper() {
return new CBServerConfigurationMapper<>();
}
@NotNull
@Override
protected Map<String, Object> collectServerConfigProperties(
@@ -0,0 +1,145 @@
/*
* 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.server;
import io.cloudbeaver.DBWFeatureSet;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.config.AdminServerConfig;
import io.cloudbeaver.model.config.CBAppConfig;
import io.cloudbeaver.model.config.CBServerConfig;
import io.cloudbeaver.utils.ServletAppUtils;
import io.cloudbeaver.utils.WebDataSourceUtils;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.utils.CommonUtils;
import java.util.*;
public class CBServerConfigurationMapper<C extends CBServerConfig, I extends AdminServerConfig> {
private static final Log log = Log.getLog(CBServerConfigurationMapper.class);
@NotNull
public CBServerConfigurations loadServerConfigurationsFromParams(@NotNull Map<String, Object> params) {
CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration());
C serverConfig = createServerConfiguration();
serverConfig.setServerName(CBApplication.getInstance().getServerName());
serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime());
String adminName = null;
String adminPassword = null;
if (!params.isEmpty()) { // FE can send an empty configuration
I config = createInputConfiguration(params);
adminName = config.getAdminName();
adminPassword = config.getAdminPassword();
populateConfigurations(config, serverConfig, appConfig);
}
return new CBServerConfigurations(
serverConfig,
appConfig,
adminName,
adminPassword
);
}
protected void populateConfigurations(@NotNull I input, @NotNull C serverConfig, @NotNull CBAppConfig appConfig) {
appConfig.setAnonymousAccessEnabled(input.isAnonymousAccessEnabled());
appConfig.setSupportsCustomConnections(input.isCustomConnectionsEnabled());
appConfig.setPublicCredentialsSaveEnabled(input.isPublicCredentialsSaveEnabled());
appConfig.setAdminCredentialsSaveEnabled(input.isAdminCredentialsSaveEnabled());
updateDisabledFeaturesConfig(appConfig, input.getEnabledFeatures());
// custom logic for enabling embedded drivers
updateDisabledDriversConfig(appConfig, input.getDisabledDrivers());
appConfig.setResourceManagerEnabled(input.isResourceManagerEnabled());
appConfig.setSecretManagerEnabled(input.isSecretManagerEnabled());
if (CommonUtils.isEmpty(input.getEnabledAuthProviders())) {
// All of them
appConfig.setEnabledAuthProviders(new String[0]);
} else {
appConfig.setEnabledAuthProviders(input.getEnabledAuthProviders().toArray(new String[0]));
}
appConfig.setDefaultNavigatorSettings(
CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
serverConfig.setServerName(input.getServerName());
serverConfig.setMaxSessionIdleTime(input.getSessionExpireTime());
if (input.getForceHttps() != null) {
serverConfig.setForceHttps(input.getForceHttps());
}
if (input.getSupportedHosts() != null) {
serverConfig.setSupportedHosts(input.getSupportedHosts());
}
if (input.getBindSessionToIp() != null) {
serverConfig.setBindSessionToIp(input.getBindSessionToIp());
}
}
private void updateDisabledFeaturesConfig(@NotNull CBAppConfig appConfig, @NotNull List<String> enabledFeatures) {
Set<String> enabledIds = new LinkedHashSet<>(enabledFeatures);
appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0]));
String[] disabledFeatures = ServletAppUtils.getServletApplication().getFeatureRegistry().getWebFeatures().stream()
.map(DBWFeatureSet::getId)
.filter(id -> !enabledIds.contains(id))
.toArray(String[]::new);
appConfig.setDisabledFeatures(disabledFeatures);
}
// we disable embedded drivers by default and enable it in enabled drivers list
// that's why we need so complicated logic for disabling drivers
private void updateDisabledDriversConfig(@NotNull CBAppConfig appConfig, @NotNull String[] disabledDriversConfig) {
Set<String> disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig));
Set<String> enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers()));
// remove all disabled embedded drivers from enabled drivers list
enabledIds.removeAll(disabledIds);
// enable embedded driver if it is not in disabled drivers list
for (String driverId : appConfig.getDisabledDrivers()) {
if (disabledIds.contains(driverId)) {
// driver is also disabled
continue;
}
// driver is removed from disabled list
// we need to enable if it is embedded
try {
DBPDriver driver = WebDataSourceUtils.getDriverById(driverId);
if (driver.isEmbedded()) {
enabledIds.add(driverId);
}
} catch (DBWebException e) {
log.error("Failed to find driver by id", e);
}
}
appConfig.setDisabledDrivers(disabledDriversConfig);
appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new));
}
@NotNull
protected C createServerConfiguration() {
return (C) new CBServerConfig();
}
@NotNull
protected I createInputConfiguration(@NotNull Map<String, Object> params) {
return (I) new AdminServerConfig(params);
}
}
@@ -0,0 +1,30 @@
/*
* 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.server;
import io.cloudbeaver.model.config.CBAppConfig;
import io.cloudbeaver.model.config.CBServerConfig;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
public record CBServerConfigurations(
@NotNull CBServerConfig serverConfig,
@NotNull CBAppConfig appConfig,
@Nullable String adminName,
@Nullable String adminPassword
) {
}
@@ -202,6 +202,43 @@ public class CBSessionManager implements WebAppSessionManager {
return httpSession.getId();
}
/**
* Invalidates the current HTTP session, creates a new one, and binds a new {@link WebSession} to it.
*/
@NotNull
public WebSession rotateSession(
@NotNull HttpServletRequest request,
@NotNull WebSession oldWebSession
) throws DBWebException {
HttpSession oldHttpSession = request.getSession(false);
if (oldHttpSession != null) {
oldHttpSession.invalidate();
}
String newSessionId = request.getSession(true).getId();
String locale = oldWebSession.getLocale();
String remoteAddr = oldWebSession.getLastRemoteAddr();
String remoteUserAgent = oldWebSession.getLastRemoteUserAgent();
var requestInfo = new WebHttpRequestInfo(newSessionId, locale, remoteAddr, remoteUserAgent);
WebSession newWebSession;
try {
newWebSession = createWebSessionImpl(requestInfo);
} catch (DBException e) {
throw new DBWebException(e);
}
oldWebSession.migrateEventHandlersTo(newWebSession);
oldWebSession.getEventsFilter().migrateTo(newWebSession.getEventsFilter());
String oldSessionId = oldWebSession.getSessionId();
synchronized (sessionMap) {
sessionMap.remove(oldSessionId);
sessionMap.put(newSessionId, newWebSession);
}
oldWebSession.close(false, false);
log.debug("Session rotated '" + oldSessionId + "' -> '" + newSessionId + "'");
return newWebSession;
}
/**
* Returns not expired session from cache, or restore it.
*
@@ -288,13 +325,12 @@ public class CBSessionManager implements WebAppSessionManager {
@Nullable
public WebSession findWebSession(@NotNull HttpServletRequest request) {
String sessionId = getSessionId(request);
WebSession webSession;
synchronized (sessionMap) {
var session = sessionMap.get(sessionId);
if (session instanceof WebSession) {
return (WebSession) session;
}
return null;
webSession = (session instanceof WebSession) ? (WebSession) session : null;
}
return webSession;
}
@Nullable
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Server
Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true
Bundle-Version: 26.1.2.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 26.1.4.qualifier
Bundle-Release-Date: 20260803
Bundle-RequiredExecutionEnvironment: JavaSE-21
Bundle-ActivationPolicy: lazy
Require-Bundle: org.jkiss.dbeaver.model.sql;visibility:=reexport,
@@ -45,6 +45,9 @@
<eventHandler class="io.cloudbeaver.server.events.WSDataSourceUpdatedEventHandlerImpl">
<topic id="cb_datasource"/>
</eventHandler>
<eventHandler class="io.cloudbeaver.server.events.WSNetworkProfileEventHandler">
<topic id="cb_profiles"/>
</eventHandler>
<eventHandler class="io.cloudbeaver.server.events.WSFolderUpdatedEventHandlerImpl">
<topic id="cb_datasource_folder"/>
</eventHandler>
+1 -1
View File
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.server</artifactId>
<version>26.1.2-SNAPSHOT</version>
<version>26.1.4-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -570,6 +570,31 @@ type ConnectionInfo {
"List of tools that can be used with this connection. Returns empty list if no tools are available"
tools: [String!]! @since(version: "24.1.3")
connectionType: ID! @since(version: "26.1.2")
"Driver configuration"
driverConfiguration: DriverConfiguration! @since(version: "26.1.2")
}
"Database-dependent metadata describing features supported by the driver"
type DriverConfiguration {
"Supported 'on duplicate key' replace methods for this connection's SQL dialect"
supportedInsertReplaceMethods: [DictionaryValueInfo!]!
"Whether the connection's database supports bulk load during data import"
supportsBulkLoad: Boolean!
"Whether the connection's database supports transactions"
supportsTransactions: Boolean!
}
"Generic dictionary value: an identifier with a human readable name and an optional description"
type DictionaryValueInfo {
"Value ID"
id: ID!
"Human readable value name"
name: String!
"Value description"
description: String
}
type ConnectionFolderInfo {
@@ -633,6 +658,39 @@ type LogEntry {
stackTrace: String
}
type ConnectionType @since(version: "26.1.2") {
"Unique identifier of the connection type"
id: ID!
"Human-readable name of the connection type"
name: String!
"Description of the connection type"
description: String
"Color of the connection type (for light theme)"
colorLight: String!
"Alternative color of the connection type (for dark theme)"
colorDark: String
"Is autocommit enabled for this connection type"
autocommit: Boolean!
"Confirm execution of queries for this connection type"
confirmExecute: Boolean!
"Confirm data change operations (INSERT, UPDATE, DELETE) for this connection type"
confirmDataChange: Boolean!
"Smart commit mode is enabled for this connection type"
smartCommit: Boolean!
"Recover smart commit mode is enabled for this connection type"
smartCommitRecover: Boolean!
"Auto-close transactions is enabled for this connection type"
autoCloseTransactions: Boolean!
"Auto-close idle transactions period in seconds for this connection type"
closeIdleTransactionPeriod: Int!
"Auto-close connections is enabled for this connection type"
autoCloseConnections: Boolean!
"Auto-close idle connections period in seconds for this connection type"
closeIdleConnectionPeriod: Int!
"Specifies whether this connection type is system-defined or user-defined. System-defined connection types cannot be deleted."
predefined: Boolean!
}
####################################################
# Input types
####################################################
@@ -745,6 +803,8 @@ input ConnectionConfig {
defaultSchemaName: String @since(version: "25.0.5") @deprecated(reason: "25.2.1 use expertPropertyValues instead")
"Default user settings for connection"
defaultUserPreferences: Object @since(version: "26.0.2")
connectionType: ID @since(version: "26.1.2")
}
####################################################
@@ -790,6 +850,8 @@ extend type Query {
"Returns connection driver properties based on configuration"
listConnectionDriverProperties( projectId: ID!, config: ConnectionConfig! ): [ObjectPropertyInfo!]!
listConnectionTypes: [ConnectionType!]! @since(version: "26.1.2")
}
extend type Mutation {
@@ -31,6 +31,10 @@ enum CBServerEventId {
"Open URL request in client"
cb_open_url @since(version: "25.3.1")
"User-specific object settings updated"
cb_object_settings_updated @since(version: "26.1.4")
"User-specific object settings deleted"
cb_object_settings_deleted @since(version: "26.1.4")
}
"Events sent by client"
@@ -57,6 +61,7 @@ enum CBEventTopic {
cb_workspace_configuration @since(version: "25.1.1")
cb_notification @since(version: "25.2.1")
cb_session_action @since(version: "25.3.1")
cb_object_settings @since(version: "26.1.4")
}
"Base server event interface"
@@ -178,6 +183,26 @@ type WSOpenUrlEvent implements CBServerEvent @since(version: "25.3.1") {
actionId: String!
}
"Type of the object that owns user-specific settings"
enum WSObjectSettingsType @since(version: "26.1.4") {
datasource
project
}
"User-specific object settings change event (e.g. navigator view)"
type WSObjectSettingsEvent implements CBServerEvent @since(version: "26.1.4") {
id: CBServerEventId!
topicId: CBEventTopic!
"Type of the object the settings belong to"
smObjectType: WSObjectSettingsType!
"Project of the object"
projectId: String
"Id of the object"
objectId: String!
"Ids of the settings that were changed"
settingIds: [String!]!
}
extend type Query {
emptyEvent: Boolean
}
@@ -68,19 +68,39 @@ input SQLDataFilter {
limit: Int
constraints: [ SQLDataFilterConstraint ]
"When true constraints are combined with OR instead of AND"
anyConstraint: Boolean
where: String
orderBy: String
}
type SQLResultReference @since(version: "26.1.1") {
"Pair (source-row column index, target-entity column name) used to build a navigation filter"
type SQLReferenceColumnMapping @since(version: "26.1.2") {
"Position in the result-set bindings"
sourceColumnIndex: Int!
"Column name in the source result set"
sourceColumnName: String!
"Ordinal position of the column in the target entity definition"
targetColumnIndex: Int!
"Column name in the target entity"
targetColumnName: String!
}
type SQLResultAssociation @since(version: "26.1.2") {
"True for reverse references (incoming FKs)"
reference: Boolean!
"Name of the association (foreign key)"
associationName: String!
"Fully qualified name of the entity"
"Name of the catalog the target entity belongs to"
targetCatalogName: String
"Name of the schema the target entity belongs to"
targetSchemaName: String
"Plain name of the target entity"
targetEntityName: String
"Navigator node URI of the target entity, suitable for opening it directly in the UI. Null if the entity is not present in the navigator tree"
nodePath: String
"Indices of result-set columns that participate as the source-side attributes of this reference"
columnIndexList: [Int!]!
targetNodePath: String
"List of column mappings between the source result set and the target entity"
columnMapping: [SQLReferenceColumnMapping!]!
}
type SQLResultColumn {
@@ -133,11 +153,6 @@ type SQLResultSet {
"Returns list of rows in the result set. Each row contains data and metadata"
rowsWithMetaData: [SQLResultRowMetaData] @since(version: "23.3.5")
"Forward foreign keys navigable from this result set"
associations: [SQLResultReference!]! @since(version: "26.1.1")
"Reverse references (incoming FKs from other entities) navigable from this result set"
references: [SQLResultReference!]! @since(version: "26.1.1")
# True means that resultset was generated by single entity query
# New rows can be added, old rows can be deleted
singleEntity: Boolean!
@@ -281,6 +296,8 @@ type SQLQueryGenerator {
input SQLQueryGeneratorOptions {
useFullyQualifiedNames: Boolean!
compactSql: Boolean!
"Include nested objects, comments and permissions in generated DDL (since 26.0.4)"
showFullDdl: Boolean
}
####################################################
@@ -328,6 +345,15 @@ type TransactionLogInfos {
extend type Query {
"Foreign-key navigation metadata for the given result set. Set isReference to true for reverse references, false for forward associations, or omit it to get both"
sqlResultAssociations(
projectId: ID,
connectionId: ID!,
contextId: ID!,
resultsId: ID!,
isReference: Boolean
): [SQLResultAssociation!]! @since(version: "26.1.2")
"Returns SQL dialect info for the specified connection"
sqlDialectInfo( projectId: ID, connectionId: ID! ): SQLDialectInfo
@@ -373,7 +399,7 @@ extend type Query {
options: Object @deprecated(reason: "use generatorOptions (26.0.3)")
nodePathList: [String!]!,
generatorOptions: SQLQueryGeneratorOptions
): String!
): String! @deprecated(reason: "use asyncSqlGenerateEntityQuery")
"""
Returns an auto-generated SQL query based on provided rows data.
@@ -467,19 +493,6 @@ extend type Mutation {
dataFormat: ResultDataFormat
): AsyncTaskInfo!
"Creates async task for navigating a foreign-key reference exposed in SQLResultSet.references. Pass associationName, isReference and any one of the chosen reference's columnIndices (used to locate the source entity)."
asyncSqlNavigateForeignKey(
projectId: ID,
connectionId: ID!,
contextId: ID!,
resultsId: ID!,
row: SQLResultRow!,
columnIndex: Int!,
associationName: String!,
isReference: Boolean!,
dataFormat: ResultDataFormat
): AsyncTaskInfo! @since(version: "26.1.1")
"Returns transaction log info for the specified project, connection and context"
getTransactionLogInfo(
projectId: ID!,
@@ -582,6 +595,13 @@ extend type Mutation {
"Returns row count for async SQL row data count task"
asyncSqlRowDataCountResult(taskId: ID!): Int!
"Creates async task to generate entity SQL. Generated SQL is returned in the taskResult field"
asyncSqlGenerateEntityQuery(
generatorId: String!,
nodePathList: [String!]!,
generatorOptions: SQLQueryGeneratorOptions
): AsyncTaskInfo!
"Reads dynamic trace from provided database results"
sqlGetDynamicTrace(
projectId: ID,
@@ -21,8 +21,10 @@ import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBConstants;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBConstants;
import org.jkiss.dbeaver.model.DatabaseURL;
import org.jkiss.dbeaver.model.connection.*;
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative;
import org.jkiss.dbeaver.model.meta.Property;
@@ -180,13 +182,26 @@ public class WebDatabaseDriverInfo {
@NotNull
@Property
public WebPropertyInfo[] getDriverProperties() throws DBWebException {
DBPConnectionConfiguration cfg = new DBPConnectionConfiguration();
cfg.setUrl(CommonUtils.notEmpty(driver.getSampleURL()));
cfg.setHostName(CBConstants.HOST_LOCALHOST);
cfg.setHostPort(driver.getDefaultPort());
cfg.setDatabaseName(driver.getDefaultDatabase());
cfg.setUrl(driver.getConnectionURL(cfg));
return WebServiceUtils.getDriverProperties(webSession, driver, null, cfg);
try {
DBPConnectionConfiguration cfg = new DBPConnectionConfiguration();
cfg.setUrl(CommonUtils.notEmpty(driver.getSampleURL()));
DatabaseURL.Pattern dbUrlPattern = DatabaseURL.getUrlPattern(CommonUtils.notEmpty(driver.getSampleURL()));
for (String propName : dbUrlPattern.getMandatoryPropertyNames()) {
switch (propName) {
case DBConstants.PROP_HOST -> cfg.setHostName(CBConstants.HOST_LOCALHOST);
case DBConstants.PROP_PORT -> cfg.setHostPort(CommonUtils.notNull(driver.getDefaultPort(), "0"));
case DBConstants.PROP_SERVER -> cfg.setServerName(CommonUtils.notNull(driver.getDefaultServer(), propName));
case DBConstants.PROP_USER -> cfg.setUserName(CommonUtils.notNull(driver.getDefaultUser(), propName));
case DBConstants.PROP_DATABASE, DBConstants.PROP_FOLDER, DBConstants.PROP_FILE ->
cfg.setDatabaseName(CommonUtils.notNull(driver.getDefaultDatabase(), propName));
default -> log.debug("Unexpected mandatory URL property " + propName);
}
}
cfg.setUrl(driver.getConnectionURL(cfg));
return WebServiceUtils.getDriverProperties(webSession, driver, null, cfg);
} catch (DBException e) {
throw new DBWebException("Error obtaining driver properties", e);
}
}
@Property
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -19,6 +19,7 @@ package io.cloudbeaver.server.events;
import io.cloudbeaver.server.BaseWebPlatform;
import io.cloudbeaver.server.WebAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.model.websocket.WSEventHandler;
@@ -33,7 +34,10 @@ public class WSDeleteTempFileHandler implements WSEventHandler<WSEventDeleteTemp
private static final Log log = Log.getLog(WSDeleteTempFileHandler.class);
public void resetTempFolder(String sessionId) {
public void resetTempFolder(@Nullable String sessionId) {
if (sessionId == null) {
return;
}
Path path = WebAppUtils.getWebPlatform()
.getTempFolder(new VoidProgressMonitor(), BaseWebPlatform.TEMP_FILE_FOLDER)
.resolve(sessionId);
@@ -0,0 +1,56 @@
/*
* 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.server.events;
import io.cloudbeaver.WebSessionProjectImpl;
import io.cloudbeaver.model.session.BaseWebSession;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.event.datasource.WSNetworkProfileEvent;
import org.jkiss.dbeaver.runtime.DBWorkbench;
public class WSNetworkProfileEventHandler extends WSDefaultEventHandler<WSNetworkProfileEvent> {
private static final Log log = Log.getLog(WSNetworkProfileEventHandler.class);
@Override
protected void updateSessionData(@NotNull BaseWebSession activeUserSession, @NotNull WSNetworkProfileEvent event) {
if (event.getProjectId() != null) {
if (activeUserSession instanceof WebSession webSession) {
WebSessionProjectImpl project = webSession.getProjectById(event.getProjectId());
if (project == null) {
log.debug("Project " + event.getProjectId() + " is not found in session " + webSession.getSessionId());
return;
}
project.getDataSourceRegistry().getNetworkProfiles().reloadProfiles();
} else {
super.updateSessionData(activeUserSession, event);
}
} else {
DBWorkbench.getPlatform().getNetworkProfiles().reloadProfiles();
super.updateSessionData(activeUserSession, event);
}
}
@Override
protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSNetworkProfileEvent event) {
if (event.getProjectId() != null && !activeUserSession.isProjectAccessible(event.getProjectId())) {
return false;
}
return super.isAcceptableInSession(activeUserSession, event);
}
}
@@ -33,12 +33,16 @@ public class CBClientEventProcessor {
private static final Log log = Log.getLog(CBClientEventProcessor.class);
final BaseWebSession webSession;
volatile BaseWebSession webSession;
public CBClientEventProcessor(@NotNull BaseWebSession webSession) {
this.webSession = webSession;
}
void setWebSession(@NotNull BaseWebSession webSession) {
this.webSession = webSession;
}
public void process(@Nullable String message) {
if (CommonUtils.isEmpty(message)) {
return;
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -40,16 +40,21 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
private static final int QUEUE_CAPACITY = 1000;
private final BaseWebSession webSession;
private volatile BaseWebSession webSession;
private final BlockingQueue<WSEvent> queue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
private final CBClientEventProcessor processor;
private volatile long lastPoll;
private final CBEventsLongPollingServlet servlet;
public CBEventsLongPolling(@NotNull BaseWebSession webSession) {
public CBEventsLongPolling(
@NotNull BaseWebSession webSession,
@NotNull CBEventsLongPollingServlet owner
) {
this.webSession = webSession;
this.lastPoll = System.currentTimeMillis();
this.webSession.addEventHandler(this);
this.processor = new CBClientEventProcessor(this.webSession);
this.servlet = owner;
}
@NotNull
@@ -88,6 +93,14 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
return result;
}
@Override
public void migrateToSession(@NotNull BaseWebSession newSession) {
String oldSid = this.webSession.getSessionId();
this.webSession = newSession;
this.processor.setWebSession(newSession);
servlet.rekeySession(oldSid, newSession.getSessionId());
}
@Override
public void handleWebSessionEvent(@NotNull WSEvent event) {
if (!queue.offer(event)) {
@@ -106,6 +119,7 @@ public class CBEventsLongPolling implements CBWebSessionEventHandler {
queue.clear();
}
@NotNull
@Override
public String toString() {
return "CBEventsLongPolling{" +
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* 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.
@@ -83,7 +83,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
return;
}
CBEventsLongPolling ps = getOrCreatePollSession(ws);
CBEventsLongPolling ps = getOrCreatePollSession(ws, resp);
ps.onPoll();
List<WSEvent> events = ps.pollEvents(POLL_TIMEOUT_SEC);
@@ -111,7 +111,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
return;
}
CBEventsLongPolling ps = getOrCreatePollSession(ws);
CBEventsLongPolling ps = getOrCreatePollSession(ws, resp);
ps.onUserActivity();
String json = new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
@@ -163,18 +163,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
) {
try {
BaseWebSession ws = resolveSession(req);
resp.setHeader(WSConstants.WS_SESSION_HEADER, ws.getSessionId());
ServletAppUtils.addResponseCookie(
req,
resp,
CBConstants.CB_SESSION_COOKIE_NAME,
ws.getSessionId(),
-1
);
return ws;
return resolveSession(req);
} catch (SMException e) {
log.debug("LP request rejected: access token expired");
sendError(resp, HttpConstants.CODE_TOKEN_EXPIRED, e.getMessage());
@@ -214,7 +203,7 @@ public class CBEventsLongPollingServlet extends HttpServlet {
}
@NotNull
private CBEventsLongPolling getOrCreatePollSession(@NotNull BaseWebSession ws) {
private CBEventsLongPolling getOrCreatePollSession(@NotNull BaseWebSession ws, @NotNull HttpServletResponse resp) {
final String sid = ws.getSessionId();
return sessions.compute(sid, (key, existing) -> {
@@ -222,9 +211,10 @@ public class CBEventsLongPollingServlet extends HttpServlet {
return existing;
}
CBEventsLongPolling ps = new CBEventsLongPolling(ws);
CBEventsLongPolling ps = new CBEventsLongPolling(ws, this);
ps.handleWebSessionEvent(new WSSocketConnectedEvent(ws.getApplication().getApplicationRunId()));
resp.setHeader(WSConstants.WS_SESSION_HEADER, sid);
log.debug("HTTP Long-Poll channel opened for session " + sid);
return ps;
@@ -253,7 +243,8 @@ public class CBEventsLongPollingServlet extends HttpServlet {
@NotNull
protected static WebHttpRequestInfo createRequestInfo(
@Nullable String sessionId, @NotNull HttpServletRequest req
@Nullable String sessionId,
@NotNull HttpServletRequest req
) {
return new WebHttpRequestInfo(
sessionId,
@@ -263,6 +254,13 @@ public class CBEventsLongPollingServlet extends HttpServlet {
);
}
void rekeySession(@NotNull String oldSid, @NotNull String newSid) {
CBEventsLongPolling moved = sessions.get(oldSid);
if (moved != null) {
sessions.put(newSid, moved);
}
}
/**
* Resolves a {@link BaseWebSession} from an incoming HTTP request.
* <p>
@@ -32,7 +32,11 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
private static final Log log = Log.getLog(CBEventsWebSocket.class);
@Nullable
private BaseWebSession webSession;
private volatile BaseWebSession webSession;
@Nullable
private FromUserEventHandler eventProcessor;
@Nullable
private WebSocketPingPongCallback pingPongCallback;
@Override
public void onOpen(Session session, EndpointConfig config) {
@@ -48,13 +52,31 @@ public class CBEventsWebSocket extends CBAbstractWebSocket implements CBWebSessi
log.debug("EventWebSocket connected to the " + webSession.getSessionId() + " session");
session.setMaxIdleTimeout(Duration.ofMinutes(5).toMillis());
session.addMessageHandler(String.class, new FromUserEventHandler(webSession));
session.addMessageHandler(PongMessage.class, new WebSocketPingPongCallback(webSession));
this.eventProcessor = new FromUserEventHandler(webSession);
session.addMessageHandler(String.class, eventProcessor);
this.pingPongCallback = new WebSocketPingPongCallback(webSession);
session.addMessageHandler(PongMessage.class, pingPongCallback);
CBJettyWebSocketManager.registerWebSocket(webSession.getSessionId(), this);
}
}
@Override
public void migrateToSession(@NotNull BaseWebSession newSession) {
BaseWebSession oldSession = this.webSession;
this.webSession = newSession;
if (eventProcessor != null) {
eventProcessor.setWebSession(newSession);
}
if (pingPongCallback != null) {
pingPongCallback.setWebSession(newSession);
}
if (oldSession != null) {
CBJettyWebSocketManager.migrateWebSocket(oldSession.getSessionId(), newSession.getSessionId(), this);
}
log.debug("EventWebSocket migrated to the " + newSession.getSessionId() + " session");
}
private static class FromUserEventHandler extends CBClientEventProcessor implements MessageHandler.Whole<String> {
private FromUserEventHandler(@NotNull BaseWebSession webSession) {
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -39,6 +39,25 @@ public class CBJettyWebSocketManager {
socketBySessionId.computeIfAbsent(webSessionId, key -> new CopyOnWriteArrayList<>()).add(webSocket);
}
/**
* Re-keys a live web socket from the old session id to the new one after a session rotation,
* so that keep-alive pings keep being delivered to it.
*/
public static void migrateWebSocket(
@NotNull String oldSessionId,
@NotNull String newSessionId,
@NotNull CBEventsWebSocket webSocket
) {
List<CBEventsWebSocket> oldSockets = socketBySessionId.get(oldSessionId);
if (oldSockets != null) {
oldSockets.remove(webSocket);
if (oldSockets.isEmpty()) {
socketBySessionId.remove(oldSessionId);
}
}
registerWebSocket(newSessionId, webSocket);
}
public static void sendPing() {
//remove expired sessions
WebAppSessionManager webSessionManager = WebAppUtils.getWebApplication().getSessionManager();
@@ -24,12 +24,16 @@ import org.jkiss.code.NotNull;
public class WebSocketPingPongCallback implements MessageHandler.Whole<PongMessage> {
@NotNull
private final BaseWebSession webSession;
private volatile BaseWebSession webSession;
public WebSocketPingPongCallback(@NotNull BaseWebSession webSession) {
this.webSession = webSession;
}
void setWebSession(@NotNull BaseWebSession webSession) {
this.webSession = webSession;
}
@Override
public void onMessage(PongMessage message) {
if (webSession instanceof WebHeadlessSession) {
@@ -275,6 +275,12 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
if (project == null) {
throw new DBException("Project not found:" + projectId);
}
var customConnectionsEnabled =
ServletAppUtils.getServletApplication().getAppConfiguration().isSupportsCustomConnections();
if (!customConnectionsEnabled && project.isPrivateProject()) {
throw new DBWebExceptionAccessDenied("Access to private project is denied");
}
RMProject rmProject = project.getRMProject();
for (String reqProjectPermission : requireProjectPermissions) {
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* 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.
@@ -71,7 +71,7 @@ public abstract class WebServiceServletBase extends HttpServlet {
} catch (Exception e) {
log.error(e);
errorMessage = e.getMessage();
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + errorMessage);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error processing request: " + flattenForResponse(errorMessage));
} finally {
if (WebAppUtils.getWebApplication() instanceof ApiCallInterceptor apiCallInterceptor) {
apiCallInterceptor.onApiCallEvent(
@@ -83,6 +83,17 @@ public abstract class WebServiceServletBase extends HttpServlet {
protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException;
/**
* Replaces control characters (line breaks, tabs, etc.) with spaces so the message can be safely
* embedded into the JSON error body produced by the servlet container's error handler.
*/
private String flattenForResponse(String message) {
if (message == null) {
return null;
}
return message.replaceAll("\\p{Cntrl}+", " ").trim();
}
protected Map<String, Object> getVariables(HttpServletRequest request) {
return gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE);
}
@@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
import org.jkiss.dbeaver.model.rm.RMConstants;
import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings;
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
@@ -115,6 +116,9 @@ public interface DBWServiceCore extends DBWService {
@NotNull Map<String, Object> connectionConfig
) throws DBWebException;
@NotNull
List<DBPConnectionType> getConnectionTypes(@NotNull WebSession webSession, @Nullable String id);
@WebAction
WebConnectionInfo getConnectionState(WebSession webSession, @Nullable String projectId, String connectionId) throws DBWebException;
@@ -159,17 +163,17 @@ public interface DBWServiceCore extends DBWService {
@NotNull Map<String, Object> connectionConfig
) throws DBWebException;
@WebAction
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebConnectionInfo testConnection(
@NotNull WebSession webSession,
@Nullable String projectId,
@Nullable @WebObjectId String projectId,
@NotNull Map<String, Object> connectionConfig
) throws DBWebException;
@WebAction
@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebNetworkEndpointInfo testNetworkHandler(
@NotNull WebSession webSession,
@Nullable String projectId,
@Nullable @WebObjectId String projectId,
@Nullable String connectionId,
@NotNull WebNetworkHandlerConfigInput nhConfig
) throws DBWebException;
@@ -87,6 +87,11 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
getArgumentVal(env, "projectId"),
getArgumentVal(env, "config")
)
).dataFetcher(
"listConnectionTypes", env -> getService(env).getConnectionTypes(
getWebSession(env),
getArgument(env, "id")
)
)
;
@@ -45,6 +45,7 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.SMObjectType;
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
import org.jkiss.dbeaver.model.connection.DBPConnectionType;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.exec.DBCConnectException;
import org.jkiss.dbeaver.model.navigator.DBNDataSource;
@@ -310,17 +311,32 @@ public class WebServiceCore implements DBWServiceCore {
DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource(
webSession, projectId, configInput.getConnectionId());
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
DBPConnectionConfiguration connectionConfiguration = new DBPConnectionConfiguration(testDataSource.getConnectionConfiguration());
return WebServiceUtils.getDriverProperties(
webSession,
testDataSource.getDriver(),
testDataSource,
connectionConfiguration
);
try {
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
DBPConnectionConfiguration connectionConfiguration
= new DBPConnectionConfiguration(testDataSource.getConnectionConfiguration());
return WebServiceUtils.getDriverProperties(
webSession,
testDataSource.getDriver(),
testDataSource,
connectionConfiguration
);
} catch (DBWebException e) {
log.error("Error getting driver properties", e);
return new WebPropertyInfo[0];
}
}
@NotNull
@Override
public List<DBPConnectionType> getConnectionTypes(@NotNull WebSession webSession, @Nullable String id) {
return DataSourceProviderRegistry.getInstance().getConnectionTypes().stream()
.map(ct -> id == null || id.equals(ct.getId()) ? ct : null)
.filter(Objects::nonNull)
.toList();
}
@Override
public WebConnectionInfo initConnection(
@@ -541,6 +557,9 @@ public class WebServiceCore implements DBWServiceCore {
DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource(
webSession, projectId, configInput.getConnectionId());
DataSourceDescriptor testDataSource = getDataSourceDescriptor(webSession, dataSource, configInput, project);
testDataSource.setTemporary(true);
WebConnectionInfo connectionInfo = project.addConnection(testDataSource);
connectionInfo.setSavedCredentials(configInput.getCredentials(), configInput.getNetworkHandlersConfig());
try {
ConnectionTestJob ct = new ConnectionTestJob(
testDataSource, param -> {
@@ -557,7 +576,6 @@ public class WebServiceCore implements DBWServiceCore {
}
throw new DBWebException("Connection failed", ct.getConnectError());
}
WebConnectionInfo connectionInfo = project.createConnectionInfo(testDataSource);
connectionInfo.setConnectError(ct.getConnectError());
connectionInfo.setServerVersion(ct.getServerVersion());
connectionInfo.setClientVersion(ct.getClientVersion());
@@ -565,6 +583,8 @@ public class WebServiceCore implements DBWServiceCore {
return connectionInfo;
} catch (DBException e) {
throw new DBWebException("Error connecting to database", e);
} finally {
project.removeConnection(testDataSource);
}
}
@@ -585,11 +605,7 @@ public class WebServiceCore implements DBWServiceCore {
}
testDataSource = (DataSourceDescriptor) dataSource.createCopy(dataSource.getRegistry());
WebDataSourceUtils.setConnectionConfiguration(
testDataSource.getDriver(),
testDataSource.getConnectionConfiguration(),
configInput
);
project.updateDataSourceContainerFromInput(configInput, testDataSource);
if (configInput.getSelectedSecretId() != null) {
try {
dataSource.listSharedCredentials()
@@ -33,7 +33,6 @@ import org.jkiss.dbeaver.model.struct.rdb.DBSSchema;
import org.jkiss.dbeaver.model.struct.rdb.DBSTable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -58,6 +57,7 @@ public class WebDatabaseObjectInfo {
public static final String OBJECT_FEATURE_SCHEMA = "schema";
public static final String OBJECT_FEATURE_SCRIPT = "script";
public static final String OBJECT_FEATURE_SCRIPT_EXTENDED = "scriptExtended";
public static final String OBJECT_FEATURE_SUPPORTS_FULL_DDL = "supportsFullDdl";
private final WebSession session;
private final DBSObject object;
@@ -168,10 +168,21 @@ public class WebDatabaseObjectInfo {
return features.toArray(new String[0]);
}
private void getObjectFeatures(DBSObject object, List<String> features) {
private void getObjectFeatures(@NotNull DBSObject object, @NotNull List<String> features) {
if (object instanceof DBPScriptObject) {
features.add(OBJECT_FEATURE_SCRIPT);
}
if (object instanceof DBPScriptObjectExt) {
features.add(OBJECT_FEATURE_SCRIPT_EXTENDED);
}
if (object instanceof DBPScriptObjectExt2 scriptObjectExt2 && (
scriptObjectExt2.supportsObjectDefinitionOption(DBPScriptObject.OPTION_INCLUDE_NESTED_OBJECTS)
|| scriptObjectExt2.supportsObjectDefinitionOption(DBPScriptObject.OPTION_INCLUDE_COMMENTS)
|| scriptObjectExt2.supportsObjectDefinitionOption(DBPScriptObject.OPTION_INCLUDE_PERMISSIONS))
) {
features.add(OBJECT_FEATURE_SUPPORTS_FULL_DDL);
}
boolean isDiagramSupported = true;
if (object instanceof DBPScriptObject) features.add(OBJECT_FEATURE_SCRIPT);
if (object instanceof DBPScriptObjectExt) features.add(OBJECT_FEATURE_SCRIPT_EXTENDED);
if (object instanceof DBSDataContainer) {
features.add(OBJECT_FEATURE_DATA_CONTAINER);
if (((DBSDataContainer) object).isFeatureSupported(DBSDataContainer.FEATURE_DATA_FILTER)) {
@@ -181,7 +192,9 @@ public class WebDatabaseObjectInfo {
isDiagramSupported = false;
}
}
if (object instanceof DBSDataManipulator) features.add(OBJECT_FEATURE_DATA_MANIPULATOR);
if (object instanceof DBSDataManipulator) {
features.add(OBJECT_FEATURE_DATA_MANIPULATOR);
}
if (object instanceof DBSEntity) {
features.add(OBJECT_FEATURE_ENTITY);
if (object instanceof DBSDataType
@@ -192,8 +205,12 @@ public class WebDatabaseObjectInfo {
features.add(OBJECT_FEATURE_RELATIONAL_ENTITY);
}
}
if (object instanceof DBSSchema) features.add(OBJECT_FEATURE_SCHEMA);
if (object instanceof DBSCatalog) features.add(OBJECT_FEATURE_CATALOG);
if (object instanceof DBSSchema) {
features.add(OBJECT_FEATURE_SCHEMA);
}
if (object instanceof DBSCatalog) {
features.add(OBJECT_FEATURE_CATALOG);
}
if (object instanceof DBSObjectContainer objectContainer) {
features.add(OBJECT_FEATURE_OBJECT_CONTAINER);
try {
@@ -279,7 +279,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
// in that way all related scripts will use the same context and overwrite the user's define context
// So why we need restore the default execution context after refreshing datasource
DBCExecutionContext defaultContext = DBUtils.getDefaultContext(refreshableObject, false);
DBCExecutionContextDefaults contextDefaults = defaultContext.getContextDefaults();
DBCExecutionContextDefaults<?, ?> contextDefaults = defaultContext.getContextDefaults();
refreshableObject.refreshObject(monitor);
if (contextDefaults != null && contextDefaults.getDefaultSchema() != null
&& contextDefaults.getDefaultCatalog() != null) {
@@ -603,8 +603,8 @@ public class WebServiceNavigator implements DBWServiceNavigator {
}
private void checkProjectEditAccess(@NotNull DBNNode node, @NotNull WebSession session) throws DBException {
BaseWebProjectImpl project = (BaseWebProjectImpl) node.getOwnerProject();
if (project == null || !hasNodeEditPermission(session, node, project.getRMProject())) {
var project = node.getOwnerProject();
if (!(project instanceof BaseWebProjectImpl bwp) || !hasNodeEditPermission(session, node, bwp.getRMProject())) {
throw new DBException("Access denied");
}
}
@@ -81,6 +81,15 @@ public interface DBWServiceSQL extends DBWService {
@NotNull WebSQLGeneratorOptions options
) throws DBWebException;
@NotNull
@WebAction
WebAsyncTaskInfo asyncGenerateEntityQuery(
@NotNull WebSession session,
@NotNull String generatorId,
@NotNull List<String> nodePathList,
@NotNull WebSQLGeneratorOptions options
) throws DBWebException;
@NotNull
@WebAction
String sqlGenerateResultSetQuery(
@@ -127,15 +136,12 @@ public interface DBWServiceSQL extends DBWService {
@Nullable WebDataFormat dataFormat) throws DBWebException;
@WebAction
WebAsyncTaskInfo asyncNavigateForeignKey(
List<WebSQLQueryResultAssociation> getSqlResultAssociations(
@NotNull WebSession webSession,
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull WebSQLResultsRow row,
int columnIndex,
@NotNull String associationName,
boolean isReference,
@Nullable WebDataFormat dataFormat) throws DBException;
@Nullable Boolean isReference
) throws DBException;
/**
* Reads dynamic trace from provided database results.
@@ -41,6 +41,7 @@ public class WebSQLDataFilter {
private int offset;
private int limit;
private String where;
private boolean anyConstraint;
private final List<WebSQLDataFilterConstraint> constraints = new ArrayList<>();
public WebSQLDataFilter() {
@@ -57,9 +58,10 @@ public class WebSQLDataFilter {
this.limit = MAX_ROWS_NUMBER;
}
this.where = CommonUtils.toString(filterProps.get("where"), null);
this.anyConstraint = CommonUtils.toBoolean(filterProps.get("anyConstraint"));
Object constraints = filterProps.get("constraints");
if (constraints instanceof Collection) {
for (Object constrItem : (Collection<?>) constraints) {
if (constraints instanceof Collection<?> col) {
for (Object constrItem : col) {
if (constrItem instanceof Map) {
this.constraints.add(
new WebSQLDataFilterConstraint((Map<String, Object>) constrItem));
@@ -92,10 +94,18 @@ public class WebSQLDataFilter {
return where;
}
/**
* When true constraints are combined with OR instead of AND
*/
public boolean isAnyConstraint() {
return anyConstraint;
}
@NotNull
public static WebSQLDataFilter from(@NotNull DBDDataFilter filter) {
var webFilter = new WebSQLDataFilter();
webFilter.where = filter.getWhere();
webFilter.anyConstraint = filter.isAnyConstraint();
for (DBDAttributeConstraint constraint : filter.getConstraints()) {
webFilter.constraints.add(WebSQLDataFilterConstraint.from(constraint));
}
@@ -106,6 +116,7 @@ public class WebSQLDataFilter {
public DBDDataFilter makeDataFilter(@Nullable WebSQLResultsInfo resultInfo) throws DBException {
DBDDataFilter dataFilter = new DBDDataFilter();
dataFilter.setWhere(where);
dataFilter.setAnyConstraint(anyConstraint);
if (CommonUtils.isEmpty(constraints)) {
return dataFilter;
}
@@ -139,14 +150,42 @@ public class WebSQLDataFilter {
private void fillEmptyConstrains(@NotNull List<DBDAttributeConstraint> emptyConstraints) throws DBException {
for (WebSQLDataFilterConstraint webConstr : constraints) {
if (webConstr.getAttributePosition() >= emptyConstraints.size()) {
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", webConstr.getAttributePosition()));
DBDAttributeConstraint dbConstr = findConstraintToFill(emptyConstraints, webConstr);
if (dbConstr.hasCondition()) {
// The attribute already has a condition from a previous constraint
dbConstr = new DBDAttributeConstraint(dbConstr);
dbConstr.reset();
emptyConstraints.add(dbConstr);
}
DBDAttributeConstraint dbConstr = emptyConstraints.get(webConstr.getAttributePosition());
fillEmptyConstraint(dbConstr, webConstr);
}
}
@NotNull
private DBDAttributeConstraint findConstraintToFill(
@NotNull List<DBDAttributeConstraint> emptyConstraints,
@NotNull WebSQLDataFilterConstraint webConstr
) throws DBException {
Integer attributePosition = webConstr.getAttributePosition();
if (attributePosition != null) {
if (attributePosition < 0 || attributePosition >= emptyConstraints.size()) {
throw new DBException(MessageFormat.format("Incorrect column position ''{0}'' in order clause", attributePosition));
}
return emptyConstraints.get(attributePosition);
}
// try by attribute name
String attributeName = webConstr.getAttributeName();
if (!CommonUtils.isEmpty(attributeName)) {
for (DBDAttributeConstraint dbConstr : emptyConstraints) {
if (CommonUtils.equalObjects(dbConstr.getAttributeName(), attributeName)) {
return dbConstr;
}
}
throw new DBException(MessageFormat.format("Constraint attribute ''{0}'' not found in result set", attributeName));
}
throw new DBException("Constraint must specify either attributePosition or attributeName");
}
@NotNull
private DBDAttributeConstraint fillEmptyConstraint(@NotNull DBDAttributeConstraint dbConstr,
@NotNull WebSQLDataFilterConstraint webConstr) {
@@ -168,6 +207,7 @@ public class WebSQLDataFilter {
return dbConstr;
}
@NotNull
private List<DBDAttributeConstraint> getDbdConstraints() {
List<DBDAttributeConstraint> dbConstraints = new ArrayList<>();
for (WebSQLDataFilterConstraint constraint : constraints) {
@@ -182,7 +222,10 @@ public class WebSQLDataFilter {
return dbConstraints;
}
private static int getOriginalVisualPosition(WebSQLDataFilterConstraint constraint, String attributeName) {
private static int getOriginalVisualPosition(
@NotNull WebSQLDataFilterConstraint constraint,
@NotNull String attributeName
) {
Integer attributePosition = constraint.getAttributePosition();
if (CommonUtils.isEmpty(attributeName)) {
@@ -18,6 +18,7 @@ package io.cloudbeaver.service.sql;
public record WebSQLGeneratorOptions(
boolean useFullyQualifiedNames,
boolean compactSql
boolean compactSql,
boolean showFullDdl
) {
}
@@ -479,7 +479,7 @@ public class WebSQLProcessor implements WebSessionProvider {
sendTransactionalEvent(contextInfo);
}
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(webSession);
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet();
updatedResultSet.setResultsInfo(resultsInfo);
updatedResultSet.setColumns(resultsInfo.getAttributes());
@@ -651,7 +651,7 @@ public class WebSQLProcessor implements WebSessionProvider {
DBDAttributeBinding[] allAttributes = resultsInfo.getAttributes();
DBDAttributeBinding[] keyAttributes = rowIdentifier.getAttributes().toArray(new DBDAttributeBinding[0]);
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(webSession);
WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet();
updatedResultSet.setResultsInfo(resultsInfo);
updatedResultSet.setColumns(resultsInfo.getAttributes());
@@ -1131,7 +1131,7 @@ public class WebSQLProcessor implements WebSessionProvider {
@NotNull WebSQLExecuteInfo executeInfo,
@NotNull DBDDataFilter filter
) throws DBException {
if (!filter.getConstraints().isEmpty() || !CommonUtils.isEmpty(filter.getWhere())) {
if (filter.getConstraintsCount() > 0 || !CommonUtils.isEmpty(filter.getWhere())) {
StringBuilder where = new StringBuilder();
SQLUtils.appendConditionString(
filter,
@@ -1268,10 +1268,23 @@ public class WebSQLProcessor implements WebSessionProvider {
throws DBException {
if (cellRow instanceof Map<?, ?> variables) {
if (variables.get(FILE_ID) != null) {
Path path = WebAppUtils.getWebPlatform()
String fileId = variables.get(FILE_ID).toString();
try {
// file id must be UUID
UUID.fromString(fileId);
} catch (IllegalArgumentException e) {
throw new DBException("File ID is invalid");
}
Path uploadFolder = WebAppUtils.getWebPlatform()
.getTempFolder(webSession.getProgressMonitor(), TEMP_FILE_FOLDER)
.resolve(webSession.getSessionId())
.resolve(variables.get(FILE_ID).toString());
.normalize();
Path path = uploadFolder.resolve(fileId).normalize();
// ensure the resolved path stays within the session upload directory
if (!path.startsWith(uploadFolder)) {
throw new DBException("File ID is invalid");
}
try {
var file = Files.newInputStream(path);
@@ -54,7 +54,7 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver {
private final WebDataFormat dataFormat;
@Nullable
private final DBDDataFilter dataFilter;
private final WebSQLQueryResultSet webResultSet;
private final WebSQLQueryResultSet webResultSet = new WebSQLQueryResultSet();
private DBDAttributeBinding[] bindings;
private DBCTrace trace;
@@ -71,7 +71,6 @@ class WebSQLQueryDataReceiver implements DBDDataReceiver {
this.dataContainer = dataContainer;
this.dataFormat = dataFormat;
this.dataFilter = dataFilter;
this.webResultSet = new WebSQLQueryResultSet(contextInfo.getProcessor().getWebSession());
rowLimit = ServletAppUtils.getServletApplication()
.getAppConfiguration()
.getResourceQuota(WebSQLConstants.QUOTA_PROP_ROW_LIMIT);
@@ -21,19 +21,20 @@ import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBPEvaluationContext;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.struct.DBSEntity;
import org.jkiss.dbeaver.model.struct.DBSEntityAssociation;
import org.jkiss.dbeaver.model.struct.DBSEntityConstraint;
import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog;
import org.jkiss.dbeaver.model.struct.rdb.DBSSchema;
import java.util.List;
public class WebSQLQueryResultReference {
public class WebSQLQueryResultAssociation {
private static final Log log = Log.getLog(WebSQLQueryResultReference.class);
private static final Log log = Log.getLog(WebSQLQueryResultAssociation.class);
@Nullable
private final WebSession session;
@@ -41,18 +42,23 @@ public class WebSQLQueryResultReference {
private final DBSEntityAssociation association;
private final boolean reverse;
@NotNull
private final List<Integer> columnIndexList;
private final List<WebSQLReferenceColumnMapping> columnMapping;
public WebSQLQueryResultReference(
public WebSQLQueryResultAssociation(
@Nullable WebSession session,
@NotNull DBSEntityAssociation association,
boolean reverse,
@NotNull List<Integer> columnIndexList
@NotNull List<WebSQLReferenceColumnMapping> columnMapping
) {
this.session = session;
this.association = association;
this.reverse = reverse;
this.columnIndexList = columnIndexList;
this.columnMapping = columnMapping;
}
@Property
public boolean isReference() {
return reverse;
}
@NotNull
@@ -63,18 +69,36 @@ public class WebSQLQueryResultReference {
@Nullable
@Property
public String getTargetEntityName() {
public String getTargetCatalogName() {
DBSEntity targetEntity = getTargetEntity();
if (targetEntity == null) {
return null;
}
return DBUtils.getObjectFullName(targetEntity, DBPEvaluationContext.UI);
DBSCatalog catalog = DBUtils.getParentOfType(DBSCatalog.class, targetEntity);
return catalog == null ? null : catalog.getName();
}
@Nullable
@Property
public String getNodePath() {
public String getTargetSchemaName() {
DBSEntity targetEntity = getTargetEntity();
if (targetEntity == null) {
return null;
}
DBSSchema schema = DBUtils.getParentOfType(DBSSchema.class, targetEntity);
return schema == null ? null : schema.getName();
}
@Nullable
@Property
public String getTargetEntityName() {
DBSEntity targetEntity = getTargetEntity();
return targetEntity == null ? null : targetEntity.getName();
}
@Nullable
@Property
public String getTargetNodePath() {
if (session == null) {
return null;
}
@@ -94,8 +118,8 @@ public class WebSQLQueryResultReference {
@NotNull
@Property
public List<Integer> getColumnIndexList() {
return columnIndexList;
public List<WebSQLReferenceColumnMapping> getColumnMapping() {
return columnMapping;
}
@Nullable
@@ -16,7 +16,6 @@
*/
package io.cloudbeaver.service.sql;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.sql.WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -36,10 +35,6 @@ public class WebSQLQueryResultSet {
private static final Log log = Log.getLog(WebSQLQueryResultSet.class);
@Nullable
private final WebSession session;
@Nullable
private DBDAttributeBinding[] referencesBindings;
private WebSQLQueryResultColumn[] columns;
private List<WebSQLQueryResultSetRow> rows = Collections.emptyList();
private boolean hasMoreData;
@@ -55,8 +50,7 @@ public class WebSQLQueryResultSet {
private boolean readOnly;
private String readOnlyStatus;
public WebSQLQueryResultSet(@Nullable WebSession session) {
this.session = session;
public WebSQLQueryResultSet() {
}
@Property
@@ -79,23 +73,6 @@ public class WebSQLQueryResultSet {
columns[i] = new WebSQLQueryResultColumn(bindings[i]);
}
this.columns = columns;
this.referencesBindings = bindings;
}
@NotNull
@Property
public List<WebSQLQueryResultReference> getAssociations() {
return session != null && referencesBindings != null
? WebSQLUtils.collectAssociations(session, referencesBindings)
: Collections.emptyList();
}
@NotNull
@Property
public List<WebSQLQueryResultReference> getReferences() {
return session != null && referencesBindings != null
? WebSQLUtils.collectReferences(session, referencesBindings)
: Collections.emptyList();
}
@Property
@@ -0,0 +1,64 @@
/*
* 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.service.sql;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.meta.Property;
public class WebSQLReferenceColumnMapping {
private final int sourceColumnIndex;
@NotNull
private final String sourceColumnName;
private final int targetColumnIndex;
@NotNull
private final String targetColumnName;
public WebSQLReferenceColumnMapping(
int sourceColumnIndex,
@NotNull String sourceColumnName,
int targetColumnIndex,
@NotNull String targetColumnName
) {
this.sourceColumnIndex = sourceColumnIndex;
this.sourceColumnName = sourceColumnName;
this.targetColumnIndex = targetColumnIndex;
this.targetColumnName = targetColumnName;
}
@Property
public int getSourceColumnIndex() {
return sourceColumnIndex;
}
@NotNull
@Property
public String getSourceColumnName() {
return sourceColumnName;
}
@Property
public int getTargetColumnIndex() {
return targetColumnIndex;
}
@NotNull
@Property
public String getTargetColumnName() {
return targetColumnName;
}
}
@@ -331,23 +331,23 @@ public class WebSQLUtils {
}
@NotNull
public static List<WebSQLQueryResultReference> collectAssociations(
public static List<WebSQLQueryResultAssociation> collectAssociations(
@NotNull WebSession session,
@NotNull DBDAttributeBinding[] bindings
) {
return collectWebSQLQueryResultReference(session, bindings, false);
return collectWebSQLQueryResultAssociation(session, bindings, false);
}
@NotNull
public static List<WebSQLQueryResultReference> collectReferences(
public static List<WebSQLQueryResultAssociation> collectReferences(
@NotNull WebSession session,
@NotNull DBDAttributeBinding[] bindings
) {
return collectWebSQLQueryResultReference(session, bindings, true);
return collectWebSQLQueryResultAssociation(session, bindings, true);
}
@NotNull
private static List<WebSQLQueryResultReference> collectWebSQLQueryResultReference(
private static List<WebSQLQueryResultAssociation> collectWebSQLQueryResultAssociation(
@NotNull WebSession session,
@NotNull DBDAttributeBinding[] bindings,
boolean reverse
@@ -369,7 +369,7 @@ public class WebSQLUtils {
return idx == null ? null : bindings[idx];
};
List<WebSQLQueryResultReference> result = new ArrayList<>();
List<WebSQLQueryResultAssociation> result = new ArrayList<>();
DBRProgressMonitor monitor = session.getProgressMonitor();
for (DBSEntity entity : entities) {
try {
@@ -377,9 +377,9 @@ public class WebSQLUtils {
? DBStructUtils.readReferences(monitor, entity, attrToBinding)
: DBStructUtils.readAssociations(monitor, entity, attrToBinding);
for (DBSEntityAssociation association : source) {
List<Integer> columnIndex = collectOwnColumnIndex(monitor, association, reverse, attrToIndex);
if (columnIndex != null) {
result.add(new WebSQLQueryResultReference(session, association, reverse, columnIndex));
List<WebSQLReferenceColumnMapping> mapping = collectColumnMapping(monitor, association, reverse, attrToIndex);
if (mapping != null) {
result.add(new WebSQLQueryResultAssociation(session, association, reverse, mapping));
}
}
} catch (DBException e) {
@@ -389,38 +389,49 @@ public class WebSQLUtils {
return result;
}
/**
* Pairs source-side attributes (in our result set) with target-side attributes
*/
@Nullable
private static List<Integer> collectOwnColumnIndex(
private static List<WebSQLReferenceColumnMapping> collectColumnMapping(
@NotNull DBRProgressMonitor monitor,
@NotNull DBSEntityAssociation association,
boolean reverse,
@NotNull Map<DBSEntityAttribute, Integer> attrToIndex
) throws DBException {
DBSEntityReferrer ownSide;
if (reverse) {
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
if (!(refConstraint instanceof DBSEntityReferrer referrer)) {
return null;
}
ownSide = referrer;
} else {
if (!(association instanceof DBSEntityReferrer associationRef)) {
return null;
}
ownSide = associationRef;
}
List<? extends DBSEntityAttributeRef> attrs = ownSide.getAttributeReferences(monitor);
if (attrs == null || attrs.isEmpty()) {
if (!(association instanceof DBSEntityReferrer associationReferrer)) {
return null;
}
List<Integer> indexList = new ArrayList<>(attrs.size());
for (DBSEntityAttributeRef attrRef : attrs) {
Integer idx = attrToIndex.get(attrRef.getAttribute());
if (idx == null) {
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
if (!(refConstraint instanceof DBSEntityReferrer constraintReferrer)) {
return null;
}
DBSEntityReferrer sourceSide = reverse ? constraintReferrer : associationReferrer;
DBSEntityReferrer targetSide = reverse ? associationReferrer : constraintReferrer;
List<? extends DBSEntityAttributeRef> sourceAttrs = sourceSide.getAttributeReferences(monitor);
List<? extends DBSEntityAttributeRef> targetAttrs = targetSide.getAttributeReferences(monitor);
if (sourceAttrs == null || targetAttrs == null
|| sourceAttrs.isEmpty() || sourceAttrs.size() != targetAttrs.size()
) {
return null;
}
List<WebSQLReferenceColumnMapping> mapping = new ArrayList<>(sourceAttrs.size());
for (int i = 0; i < sourceAttrs.size(); i++) {
DBSEntityAttribute sourceAttr = sourceAttrs.get(i).getAttribute();
Integer sourceIdx = sourceAttr == null ? null : attrToIndex.get(sourceAttr);
if (sourceIdx == null) {
return null;
}
indexList.add(idx);
DBSEntityAttribute targetAttr = targetAttrs.get(i).getAttribute();
if (targetAttr == null) {
return null;
}
// getOrdinalPosition() is 1-based. Normalize to 0-based to match sourceColumnIndex
int targetIdx = targetAttr.getOrdinalPosition() - 1;
mapping.add(new WebSQLReferenceColumnMapping(
sourceIdx, sourceAttr.getName(), targetIdx, targetAttr.getName()));
}
return indexList;
return mapping;
}
}
@@ -54,6 +54,14 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
.dataFetcher("sqlDialectInfo", env ->
getService(env).getDialectInfo(getWebConnection(env))
)
.dataFetcher("sqlResultAssociations", env ->
getService(env).getSqlResultAssociations(
getWebSession(env),
getSQLContext(env),
getArgumentVal(env, "resultsId"),
getArgument(env, "isReference")
)
)
.dataFetcher("sqlListContexts", env ->
getService(env).listContexts(getWebSession(env),
getProjectReference(env),
@@ -232,21 +240,17 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
getDataFilter(env),
getDataFormat(env)
))
.dataFetcher("asyncSqlNavigateForeignKey", env ->
getService(env).asyncNavigateForeignKey(
getWebSession(env),
getSQLContext(env),
getArgumentVal(env, "resultsId"),
new WebSQLResultsRow(getArgument(env, "row")),
getArgumentVal(env, "columnIndex"),
getArgumentVal(env, "associationName"),
CommonUtils.toBoolean(env.getArgument("isReference")),
getDataFormat(env)
))
.dataFetcher("asyncSqlExecuteResults", env ->
getService(env).asyncGetQueryResults(
getWebSession(env), getArgumentVal(env, "taskId")
))
.dataFetcher("asyncSqlGenerateEntityQuery", env ->
getService(env).asyncGenerateEntityQuery(
getWebSession(env),
getArgumentVal(env, "generatorId"),
getArgumentVal(env, "nodePathList"),
getGeneratorOptions(env)
))
.dataFetcher("asyncSqlExplainExecutionPlan", env ->
getService(env).asyncSqlExplainExecutionPlan(
getSQLContext(env),
@@ -353,7 +357,7 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
}
@Override
public void addServlets(ServletApplication application, DBWServletContext servletContext) throws DBException {
public void addServlets(@NotNull ServletApplication application, @NotNull DBWServletContext servletContext) throws DBException {
servletContext.addServlet(
"sqlResultValueViewer",
new WebSQLResultServlet(application, getServiceImpl()),
@@ -432,11 +436,12 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL>
private static WebSQLGeneratorOptions getGeneratorOptions(@NotNull DataFetchingEnvironment env) {
Map<String, Object> optionsMap = getArgument(env, "generatorOptions");
if (optionsMap == null) {
return new WebSQLGeneratorOptions(true, false);
return new WebSQLGeneratorOptions(true, false, false);
}
return new WebSQLGeneratorOptions(
CommonUtils.toBoolean(optionsMap.get("useFullyQualifiedNames")),
CommonUtils.toBoolean(optionsMap.get("compactSql"))
CommonUtils.toBoolean(optionsMap.get("compactSql")),
CommonUtils.toBoolean(optionsMap.get("showFullDdl"))
);
}
@@ -36,8 +36,6 @@ import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.data.DBDAttributeBinding;
import org.jkiss.dbeaver.model.data.DBDReferenceNavigation;
import org.jkiss.dbeaver.model.data.DBDReferenceUtils;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.exec.DBCLogicalOperator;
import org.jkiss.dbeaver.model.exec.DBExecUtils;
@@ -61,7 +59,9 @@ import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor;
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLCompletionProposalComparator;
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionAnalyzer;
import org.jkiss.dbeaver.model.sql.semantics.completion.SQLQueryCompletionContext;
import org.jkiss.dbeaver.model.struct.*;
import org.jkiss.dbeaver.model.struct.DBSDataContainer;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.model.struct.DBSWrapper;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;
@@ -252,10 +252,33 @@ public class WebServiceSQL implements DBWServiceSQL {
@NotNull WebSQLGeneratorOptions options
) throws DBWebException {
List<DBSObject> objectList = getObjectListFromNodeIds(session, nodePathList);
return createAndRunGenerator(
session, generatorId, objectList,
options.useFullyQualifiedNames(), options.compactSql()
);
return createAndRunGenerator(session, generatorId, objectList, options);
}
@NotNull
@Override
public WebAsyncTaskInfo asyncGenerateEntityQuery(
@NotNull WebSession session,
@NotNull String generatorId,
@NotNull List<String> nodePathList,
@NotNull WebSQLGeneratorOptions options
) throws DBWebException {
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<>() {
@Override
public void run(@NotNull DBRProgressMonitor monitor) throws InvocationTargetException {
try {
monitor.beginTask("Generate SQL", 1);
monitor.subTask("Generate '" + generatorId + "' SQL");
List<DBSObject> objectList = getObjectListFromNodeIds(session, monitor, nodePathList);
this.result = createAndRunGenerator(monitor, generatorId, objectList, options);
} catch (Throwable e) {
throw new InvocationTargetException(e);
} finally {
monitor.done();
}
}
};
return session.createAndRunAsyncTask("Generate SQL", runnable);
}
@NotNull
@@ -270,10 +293,7 @@ public class WebServiceSQL implements DBWServiceSQL {
) throws DBWebException {
checkAndFillTruncatedData(sqlContext, resultsId, selectedRows);
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(resultsId, sqlContext, selectedRows);
return createAndRunGenerator(
webSession, generatorId, Collections.singletonList(dataProvider),
options.useFullyQualifiedNames(), options.compactSql()
);
return createAndRunGenerator(webSession, generatorId, Collections.singletonList(dataProvider), options);
}
private void checkAndFillTruncatedData(
@@ -308,8 +328,17 @@ public class WebServiceSQL implements DBWServiceSQL {
@NotNull WebSession session,
@NotNull String generatorId,
@NotNull List<DBSObject> objectList,
boolean useFullyQualifiedNames,
boolean compactSql
@NotNull WebSQLGeneratorOptions options
) throws DBWebException {
return createAndRunGenerator(session.getProgressMonitor(), generatorId, objectList, options);
}
@NotNull
private String createAndRunGenerator(
@NotNull DBRProgressMonitor monitor,
@NotNull String generatorId,
@NotNull List<DBSObject> objectList,
@NotNull WebSQLGeneratorOptions options
) throws DBWebException {
SQLGeneratorDescriptor generator = SQLGeneratorConfigurationRegistry.getInstance().getGenerator(generatorId);
if (generator == null) {
@@ -317,9 +346,14 @@ public class WebServiceSQL implements DBWServiceSQL {
}
try {
SQLGenerator<DBSObject> generatorInstance = generator.createGenerator(objectList);
generatorInstance.setFullyQualifiedNames(useFullyQualifiedNames);
generatorInstance.setCompactSQL(compactSql);
generatorInstance.run(session.getProgressMonitor());
generatorInstance.setFullyQualifiedNames(options.useFullyQualifiedNames());
generatorInstance.setCompactSQL(options.compactSql());
if (options.showFullDdl()) {
generatorInstance.setShowFullDdl(true);
generatorInstance.setShowComments(true);
generatorInstance.setShowPermissions(true);
}
generatorInstance.run(monitor);
return generatorInstance.getResult();
} catch (DBException e) {
throw new DBWebException("Error creating SQL generator", e);
@@ -331,12 +365,24 @@ public class WebServiceSQL implements DBWServiceSQL {
}
@NotNull
private List<DBSObject> getObjectListFromNodeIds(@NotNull WebSession session, @NotNull List<String> nodePathList) throws DBWebException {
private List<DBSObject> getObjectListFromNodeIds(
@NotNull WebSession session,
@NotNull List<String> nodePathList
) throws DBWebException {
return getObjectListFromNodeIds(session, session.getProgressMonitor(), nodePathList);
}
@NotNull
private List<DBSObject> getObjectListFromNodeIds(
@NotNull WebSession session,
@NotNull DBRProgressMonitor monitor,
@NotNull List<String> nodePathList
) throws DBWebException {
try {
List<DBSObject> objectList = new ArrayList<>(nodePathList.size());
DBNModel navigatorModel = session.getNavigatorModelOrThrow();
for (String nodePath : nodePathList) {
DBNNode node = navigatorModel.getNodeByPath(session.getProgressMonitor(), nodePath);
DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath);
if (node == null) {
throw new DBException("Node '" + nodePath + "' not found");
}
@@ -603,72 +649,25 @@ public class WebServiceSQL implements DBWServiceSQL {
return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("Read data from container " + nodePath, runnable);
}
@NotNull
@Override
public WebAsyncTaskInfo asyncNavigateForeignKey(
public List<WebSQLQueryResultAssociation> getSqlResultAssociations(
@NotNull WebSession webSession,
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull WebSQLResultsRow row,
int columnIndex,
@NotNull String associationName,
boolean isReference,
@Nullable WebDataFormat dataFormat
) {
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<>() {
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException {
try {
monitor.beginTask("Navigate foreign key", 1);
WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId);
DBSEntity sourceEntity = resolveSourceEntity(resultsInfo.getAttributes(), columnIndex);
DBSEntityAssociation association = isReference
? DBStructUtils.findReverseAssociationByName(monitor, sourceEntity, associationName)
: DBStructUtils.findForwardAssociationByName(monitor, sourceEntity, associationName);
WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider(
resultsId,
contextInfo,
List.of(row)
);
DBDReferenceNavigation navigation = isReference
? DBDReferenceUtils.resolveReferenceNavigation(monitor, dataProvider, association, dataProvider.getSelectedRows())
: DBDReferenceUtils.resolveAssociationNavigation(monitor, dataProvider, association, dataProvider.getSelectedRows());
if (!(navigation.getTargetEntity() instanceof DBSDataContainer targetDataContainer)) {
throw new DBWebException("Referenced entity '" + navigation.getTargetEntity().getName() + "' is not a data container");
}
WebSQLExecuteInfo executeResults = contextInfo.getProcessor().readDataFromContainer(
contextInfo,
monitor,
targetDataContainer,
null,
WebSQLDataFilter.from(navigation.getTargetFilter()),
dataFormat
);
this.result = executeResults.getStatusMessage();
this.extendedResults = executeResults;
} catch (Throwable e) {
throw new InvocationTargetException(e);
} finally {
monitor.done();
}
}
};
return webSession.createAndRunAsyncTask("Navigate foreign key from results " + resultsId, runnable);
}
@NotNull
private DBSEntity resolveSourceEntity(
@NotNull DBDAttributeBinding[] attributes,
int columnIndex
@Nullable Boolean isReference
) throws DBException {
if (columnIndex < 0 || columnIndex >= attributes.length) {
throw new DBWebException("Column index '" + columnIndex + "' is out of range");
DBDAttributeBinding[] attributes = contextInfo.getResults(resultsId).getAttributes();
if (isReference == null) {
// Both forward associations and reverse references
List<WebSQLQueryResultAssociation> associations =
new ArrayList<>(WebSQLUtils.collectAssociations(webSession, attributes));
associations.addAll(WebSQLUtils.collectReferences(webSession, attributes));
return associations;
}
DBSEntityAttribute entityAttribute = attributes[columnIndex].getEntityAttribute();
if (entityAttribute == null) {
throw new DBException("Column [" + attributes[columnIndex].getName() + "] is not bound to any entity");
}
return entityAttribute.getParentObject();
return isReference
? WebSQLUtils.collectReferences(webSession, attributes)
: WebSQLUtils.collectAssociations(webSession, attributes);
}
@NotNull
@@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2
Bundle-Vendor: DBeaver Corp
Bundle-Name: Cloudbeaver Web Service - Administration
Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true
Bundle-Version: 1.0.148.qualifier
Bundle-Release-Date: 20260706
Bundle-Version: 1.0.150.qualifier
Bundle-Release-Date: 20260803
Bundle-RequiredExecutionEnvironment: JavaSE-21
Bundle-ActivationPolicy: lazy
Require-Bundle: io.cloudbeaver.server.ce,
@@ -10,7 +10,7 @@
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.admin</artifactId>
<version>1.0.148-SNAPSHOT</version>
<version>1.0.150-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 DBeaver Corp and others
* 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.
@@ -25,6 +25,7 @@ import io.cloudbeaver.service.DBWServletContext;
import io.cloudbeaver.service.WebServiceBindingBase;
import io.cloudbeaver.service.admin.impl.WebAdminLogsServlet;
import io.cloudbeaver.service.admin.impl.WebServiceAdmin;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.utils.CommonUtils;
@@ -205,7 +206,7 @@ public class WebServiceBindingAdmin extends WebServiceBindingBase<DBWServiceAdmi
}
@Override
public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException {
public void addServlets(@NotNull CBApplication application, @NotNull DBWServletContext servletContext) throws DBException {
if(!application.isMultiuser()) {
return;
}
@@ -19,20 +19,15 @@ package io.cloudbeaver.service.admin.impl;
import io.cloudbeaver.*;
import io.cloudbeaver.auth.provider.local.LocalAuthProvider;
import io.cloudbeaver.model.WebPropertyInfo;
import io.cloudbeaver.model.config.CBAppConfig;
import io.cloudbeaver.model.config.CBServerConfig;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.registry.*;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBConstants;
import io.cloudbeaver.server.WebAppUtils;
import io.cloudbeaver.server.*;
import io.cloudbeaver.service.DBWServiceServerConfigurator;
import io.cloudbeaver.service.admin.*;
import io.cloudbeaver.service.security.SMUtils;
import io.cloudbeaver.utils.ServletAppUtils;
import io.cloudbeaver.utils.WebDataSourceUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
@@ -44,7 +39,6 @@ import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.SMAuthConfiguration;
import org.jkiss.dbeaver.model.auth.SMObjectType;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.rm.RMProjectType;
@@ -596,50 +590,12 @@ public class WebServiceAdmin implements DBWServiceAdmin {
@Override
public boolean configureServer(@NotNull WebSession webSession, @NotNull Map<String, Object> params) throws DBWebException {
try {
CBAppConfig appConfig = new CBAppConfig(CBApplication.getInstance().getAppConfiguration());
CBServerConfig serverConfig = new CBServerConfig();
serverConfig.setServerName(CBApplication.getInstance().getServerName());
serverConfig.setMaxSessionIdleTime(CBApplication.getInstance().getMaxSessionIdleTime());
String adminName = null;
String adminPassword = null;
if (!params.isEmpty()) { // FE can send an empty configuration
var config = new AdminServerConfig(params);
appConfig.setAnonymousAccessEnabled(config.isAnonymousAccessEnabled());
appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled());
appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled());
appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled());
updateDisabledFeaturesConfig(appConfig, config.getEnabledFeatures());
// custom logic for enabling embedded drivers
updateDisabledDriversConfig(appConfig, config.getDisabledDrivers());
appConfig.setResourceManagerEnabled(config.isResourceManagerEnabled());
appConfig.setSecretManagerEnabled(config.isSecretManagerEnabled());
if (CommonUtils.isEmpty(config.getEnabledAuthProviders())) {
// All of them
appConfig.setEnabledAuthProviders(new String[0]);
} else {
appConfig.setEnabledAuthProviders(config.getEnabledAuthProviders().toArray(new String[0]));
}
appConfig.setDefaultNavigatorSettings(
CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings());
adminName = config.getAdminName();
adminPassword = config.getAdminPassword();
serverConfig.setServerName(config.getServerName());
serverConfig.setMaxSessionIdleTime(config.getSessionExpireTime());
if (config.getForceHttps() != null) {
serverConfig.setForceHttps(config.getForceHttps());
}
if (config.getSupportedHosts() != null) {
serverConfig.setSupportedHosts(config.getSupportedHosts());
}
if (config.getBindSessionToIp() != null) {
serverConfig.setBindSessionToIp(config.getBindSessionToIp());
}
}
CBServerConfigurationMapper<?, ?> mapper = CBApplication.getInstance()
.getServerConfigurationController().createServerConfigurationInputMapper();
CBServerConfigurations configurations = mapper.loadServerConfigurationsFromParams(params);
String adminName = configurations.adminName();
String adminPassword = configurations.adminPassword();
if (CommonUtils.isEmpty(adminName)) {
// Grant admin permissions to the current user
WebUser curUser = webSession.getUser();
@@ -651,7 +607,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
if (CommonUtils.isEmpty(adminName)) {
// Try to get admin name from existing authentications (first one)
if (!authInfoList.isEmpty()) {
adminName = authInfoList.get(0).getUserId();
adminName = authInfoList.getFirst().getUserId();
}
}
if (CommonUtils.isEmpty(adminName)) {
@@ -666,7 +622,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
// Patch configuration by services
for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance().getWebServices(DBWServiceServerConfigurator.class)) {
try {
wsc.configureServer(CBApplication.getInstance(), webSession, serverConfig, appConfig);
wsc.configureServer(CBApplication.getInstance(), webSession, configurations.serverConfig(), configurations.appConfig());
} catch (Exception e) {
log.warn("Error configuring server by web service " + wsc.getClass().getName(), e);
}
@@ -678,8 +634,8 @@ public class WebServiceAdmin implements DBWServiceAdmin {
adminName,
adminPassword,
authInfos,
serverConfig,
appConfig,
configurations.serverConfig(),
configurations.appConfig(),
webSession
);
@@ -687,8 +643,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
if (configurationMode) {
// In config mode we always refresh because admin user doesn't exist yet
webSession.resetUserState();
}
else {
} else {
// Just reload session state
webSession.refreshUserData();
}
@@ -700,46 +655,6 @@ public class WebServiceAdmin implements DBWServiceAdmin {
return true;
}
private void updateDisabledFeaturesConfig(@NotNull CBAppConfig appConfig, @NotNull List<String> enabledFeatures) {
Set<String> enabledIds = new LinkedHashSet<>(enabledFeatures);
appConfig.setEnabledFeatures(enabledFeatures.toArray(new String[0]));
String[] disabledFeatures = ServletAppUtils.getServletApplication().getFeatureRegistry().getWebFeatures().stream()
.map(DBWFeatureSet::getId)
.filter(id -> !enabledIds.contains(id))
.toArray(String[]::new);
appConfig.setDisabledFeatures(disabledFeatures);
}
// we disable embedded drivers by default and enable it in enabled drivers list
// that's why we need so complicated logic for disabling drivers
private void updateDisabledDriversConfig(CBAppConfig appConfig, String[] disabledDriversConfig) {
Set<String> disabledIds = new LinkedHashSet<>(Arrays.asList(disabledDriversConfig));
Set<String> enabledIds = new LinkedHashSet<>(Arrays.asList(appConfig.getEnabledDrivers()));
// remove all disabled embedded drivers from enabled drivers list
enabledIds.removeAll(disabledIds);
// enable embedded driver if it is not in disabled drivers list
for (String driverId : appConfig.getDisabledDrivers()) {
if (disabledIds.contains(driverId)) {
// driver is also disabled
continue;
}
// driver is removed from disabled list
// we need to enable if it is embedded
try {
DBPDriver driver = WebDataSourceUtils.getDriverById(driverId);
if (driver.isEmbedded()) {
enabledIds.add(driverId);
}
} catch (DBWebException e) {
log.error("Failed to find driver by id", e);
}
}
appConfig.setDisabledDrivers(disabledDriversConfig);
appConfig.setEnabledDrivers(enabledIds.toArray(String[]::new));
}
@Override
public boolean setDefaultNavigatorSettings(@NotNull WebSession webSession, @NotNull DBNBrowseSettings settings) throws DBWebException {
CBApplication.getInstance().getAppConfiguration().setDefaultNavigatorSettings(settings);
@@ -0,0 +1,15 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Cloudbeaver AI Integration
Bundle-SymbolicName: io.cloudbeaver.service.ai;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Release-Date: 20260803
Bundle-Vendor: DBeaver Corp
Bundle-ActivationPolicy: lazy
Automatic-Module-Name: io.cloudbeaver.service.ai
Require-Bundle: org.jkiss.dbeaver.model.ai,
io.cloudbeaver.model,
io.cloudbeaver.server.ce
Export-Package: io.cloudbeaver.service.ai,
io.cloudbeaver.service.ai.model,
io.cloudbeaver.service.ai.model.inputs
@@ -0,0 +1,6 @@
source..=src/
output..=target/classes/
bin.includes=.,\
META-INF/,\
schema/,\
plugin.xml
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<extension point="io.cloudbeaver.service">
<service id="ai" label="AI Service" description="AI service"
class="io.cloudbeaver.service.ai.gql.WebServiceBindingAI"/>
<service id="ai.configurator" label="AI Configurator" description="AI configurator"
class="io.cloudbeaver.service.ai.gql.WebServiceAIConfigurator">
</service>
</extension>
<extension point="io.cloudbeaver.object.feature.provider">
<featureProvider id="AIFeatureProvider" class="io.cloudbeaver.service.ai.WebAIFeatureProvider"/>
</extension>
<extension point="org.jkiss.dbeaver.ws.event">
<clientEvent id="cb_client_ai_function_call_confirmation"
class="io.cloudbeaver.service.ai.model.events.WSAiFunctionCallConfirmationClientEvent"
handler="io.cloudbeaver.service.ai.model.events.WSAiFunctionCallConfirmationHandler"/>
</extension>
</plugin>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.cloudbeaver</groupId>
<artifactId>bundles</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<artifactId>io.cloudbeaver.service.ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>eclipse-plugin</packaging>
</project>
@@ -0,0 +1,58 @@
extend enum CBServerEventId {
cb_ai_chat_message_chunk @since(version: "25.1.1")
cb_ai_chat_message_error @since(version: "25.1.1")
cb_ai_chat_message @since(version: "25.3.5")
}
extend enum CBClientEventId {
cb_client_ai_function_call_confirmation @since(version: "26.0.3")
}
extend enum CBEventTopic {
cb_ai @since(version: "25.1.1")
}
type WSAIChatMessageChunkEvent implements CBServerEvent @since(version: "25.1.1") {
id: CBServerEventId!
topicId: CBEventTopic
conversationId: ID!
messageId: ID!
chunk: String
completed: Boolean!
}
type WSAIChatMessageErrorEvent implements CBServerEvent @since(version: "25.1.1") {
id: CBServerEventId!
topicId: CBEventTopic
conversationId: ID!
messageId: ID!
errorMessage: String
}
type WSAIChatMessageEvent implements CBServerEvent @since(version: "25.3.5") {
id: CBServerEventId!
topicId: CBEventTopic
conversationId: ID!
messageId: ID!
role: String!
content: String!
displayMessage: String!
time: String!
"Present only with CONFIRMATION role"
functionConfirmation: AIFunctionConfirmation
"Present only with FUNCTION role"
functionCall: AIFunctionCall
"Present only with FUNCTION role"
functionResult: AIFunctionResult
}
type WSAIFunctionCallConfirmationEvent implements CBClientEvent @since(version: "26.0.3") {
id: CBClientEventId!
topicId: CBEventTopic
conversationId: ID!
messageId: ID!
"Returns a list of confirmed functions"
confirmedFunctionCalls: [ID!]
"Returns a list of declined functions"
declinedFunctionCalls: [ID!]
}
@@ -0,0 +1,274 @@
# AI admin queries
enum AIMessageType {
SYSTEM,
USER,
ASSISTANT,
ERROR,
WARNING,
CONFIRMATION,
ATTACHMENT,
FUNCTION
}
"""
Scope of the AI chat conversation.
Can be a database connection, a current database, a current schema or a custom scope.
"""
enum AIDatabaseScope @since(version: "25.1.1") {
CURRENT_SCHEMA
CURRENT_DATABASE
CURRENT_DATASOURCE
CUSTOM
}
type AISettingsInfo @since(version: "23.2.2") {
activeEngine: ID!
defaultConfiguration: ID
language: String
}
type AIEngineInfo @since(version: "23.2.2") {
"Unique identifier of the AI engine."
id: ID!
"Name of the AI engine."
name: String!
}
type AIChatConversationInfo @since(version: "25.1.1") {
dataSourceId: DataSourceId
id: ID!
promptGeneratorId: ID
caption: String!
"Last message creation time."
time: DateTime!
messages: [AIMessage!]!
settings: AIChatConversationSettings
waitingForResponse: Boolean! @since(version: "25.1.5")
metrics: AIChatConversationMetrics @since(version: "25.3.5")
"ID of the AI engine configuration profile used for this conversation."
profile: ID @since(version: "26.1.3")
}
type AIChatConversationMetrics @since(version: "25.3.5") {
totalInputTokens: Int!
totalOutputTokens: Int!
}
type AIChatConversationSettings {
metaTransferConfirmed: Boolean!
scope: AIDatabaseScope
"If the scope is set to CUSTOM, this field is required."
customObjectIds: [ID!]
}
type AIMessage {
conversationId: ID!
id: ID!
role: AIMessageType!
content: String!
displayMessage: String!
time: DateTime!
"Present only with CONFIRMATION role"
functionConfirmation: AIFunctionConfirmation
"Present only with FUNCTION role"
functionCall: AIFunctionCall
"Present only with FUNCTION role"
functionResult: AIFunctionResult
}
type AISendChatMessageInfo @since(version: "25.1.1") {
conversation: AIChatConversationInfo!
userMessage: AIMessage!
"Message that would be received via websockets."
assistantMessage: AIMessage!
}
type AIDataSourceSettingsInfo @since(version: "25.3.3") {
"Indicates whether user confirmed meta transfer to AI for the current data source."
metaTransferConfirmed: Boolean!
"Default scope for AI operations in this data source."
scope: AIDatabaseScope
"If the scope is set to CUSTOM, this field is required."
customObjectIds: [ID!]
}
type DataSourceId @since(version: "25.1.1") {
projectId: ID!
connectionId: ID!
}
type AIConfigurationProfileInfo @since(version: "26.1.3") {
"Unique identifier of the AI engine profile."
id: ID!
"Name of the AI engine profile."
name: String!
"ID of the AI engine that this profile belongs to."
engineId: ID!
}
type AIAdminConfigurationProfileInfo @since(version: "26.1.3") {
"Unique identifier of the AI engine profile."
id: ID!
"Name of the AI engine profile."
name: String!
"ID of the AI engine that this profile belongs to."
engineId: ID!
"Configuration of the AI engine profile."
configuration: [ObjectPropertyInfo!]!
}
type AIFunction @since(version: "26.0.4") {
"Unique identifier of the function (combines toolbox ID and function ID)"
id: ID!
"Human-readable name of the function"
name: String!
"Detailed description of what the function does"
description: String
"URL or path to the function icon"
icon: String
"Indicates whether the function is a system function (can be auto-confirmed when required)"
system: Boolean!
}
type AIFunctionCall @since(version: "26.0.3") {
id: ID!
functionName: String!
arguments: Object
hint: String
}
type AIFunctionConfirmation @since(version: "26.0.3") {
functionCalls: [AIFunctionCall!]!
}
enum AIFunctionType {
ACTION
INFORMATION
}
type AIFunctionResult @since(version: "26.0.3") {
type: AIFunctionType!
value: Object!
}
input AISettingsConfig @since(version: "23.2.2") {
"Default configuration profile ID. May be null if AI wasn't configured"
defaultConfiguration: ID
"Language to be used as a default for AI responses"
language: String
}
input AIEngineConfig @since(version: "23.2.2") {
properties: Object
}
input AIDatabaseScopeInput {
id: AIDatabaseScope!
"If custom scope is selected, this field is required."
customScopes: [ID!]
}
input DataSourceIdInput @since(version: "25.1.1") {
projectId: ID!
connectionId: ID!
}
input AIChatConversationInput {
dataSourceId: DataSourceIdInput
caption: String
settings: AIChatConversationSettingsInput
"ID of the prompt generator to use for this conversation (sql - by default)."
promptGeneratorId: ID
}
input AIChatConversationSettingsInput {
metaTransferConfirmed: Boolean
scope: AIDatabaseScope
profile: ID
customObjectIds: [ID!]
}
input AIDataSourceSettingsInput @since(version: "25.3.3") {
"Flag indicating whether MCP integration is enabled for the current data source."
mcpEnabled: Boolean
"Flag indicating whether user confirmed meta transfer to AI."
metaTransferConfirmed: Boolean
"Default scope for AI operations in this data source."
scope: AIDatabaseScope
"If custom scope is selected, this field is required."
customObjectIds: [ID!]
}
input AIConfigurationProfileInput @since(version: "26.1.3") {
"Unique identifier of the AI engine profile."
profileId: ID!
"Name of the AI engine profile."
profileName: String
"ID of the AI engine that this profile belongs to."
engineId: ID
"Configuration of the AI engine profile."
configuration: AIEngineConfig
}
extend type Query @since(version: "23.2.2") {
"Returns the list of available AI engines."
aiListEngines: [AIEngineInfo!]
"Returns the global AI settings."
aiSettings: AISettingsInfo!
"Returns the properties of the specified AI engine for displaying it in the UI. In case settings are passed, fills it with defaults and returns back."
aiListEngineProperties(engineId: ID!, profileId: ID, settings : AIEngineConfig): [ObjectPropertyInfo!]!
"Return the list of available AI functions."
aiListFunctions: [AIFunction!] @since(version: "26.0.4")
"Returns the list of available AI engine configuration profiles."
aiListProfiles: [AIConfigurationProfileInfo!]! @since(version: "26.1.3")
"Returns AI chat conversation info for the specified conversation ID."
aiChatConversationInfo(conversationId: ID!, loadMetrics: Boolean): AIChatConversationInfo! @since(version: "25.1.1")
"Returns list of AI chat conversations for the specified connection."
aiListChatConversations(dataSourceId: DataSourceIdInput): [AIChatConversationInfo!]! @since(version: "25.1.1")
"Returns AI settings for the specified connection."
aiDataSourceSettings(dataSourceId: DataSourceIdInput!): AIDataSourceSettingsInfo! @since(version: "25.3.3")
}
extend type Mutation @since(version: "23.2.2") {
"Saves the global AI settings (active AI engine)."
aiSaveSettings(settings: AISettingsConfig!): AISettingsInfo!
"Saves the configuration of the specified AI engine."
aiSaveEngineConfiguration(profileId: ID!, settings : AIEngineConfig!): Boolean! @deprecated(reason: "use profiles api")
"Creates an anync task to perform AI query completion."
asyncAiPerformQueryCompletion(projectId: ID, connectionId: ID!, contextId: ID!, request: String!): AsyncTaskInfo!
"Retrieves the result of an async AI query completion task."
asyncAiPerformQueryCompletionResult(taskId: ID!): String
"Creates a new AI engine configuration profile."
aiCreateProfile(config: AIConfigurationProfileInput!): AIAdminConfigurationProfileInfo! @since(version: "26.1.3")
"Updates the configuration of the specified AI engine configuration profile."
aiUpdateProfile(config: AIConfigurationProfileInput!): AIAdminConfigurationProfileInfo! @since(version: "26.1.3")
"Deletes the specified AI engine configuration profile."
aiDeleteProfile(profileId: ID!): Boolean! @since(version: "26.1.3")
"""
Creates a new AI chat conversation.
Conversation is created in the context of a project and connection.
If the connection and project are not specified, the conversation will be created without a context.
"""
aiCreateChatConversation(config: AIChatConversationInput!): AIChatConversationInfo! @since(version: "25.1.1")
"Updates the caption of the AI chat conversation"
aiUpdateChatConversation(conversationId: ID!, config: AIChatConversationInput!): AIChatConversationInfo! @since(version: "25.1.1")
"Deletes the specified AI chat conversation"
aiDeleteChatConversation(conversationId: ID!): Boolean! @since(version: "25.1.1")
"Sends a chat message to the AI chat conversation and returns a message ID from response. The response will be received partially via websockets."
aiSendChatMessage(conversationId: ID!, prompt: String!): AISendChatMessageInfo! @since(version: "25.1.1")
"Clears the chat messages in the AI chat conversation. The messages will be removed from the conversation."
aiClearLastChatMessages(conversationId: ID!, messageId: ID!): Boolean! @since(version: "25.1.1")
"Saves AI settings (e.g. supporting confirming metadata transfer) for the specified connection."
aiSaveDataSourceSettings(dataSourceId: DataSourceIdInput!, settings: AIDataSourceSettingsInput!): AIDataSourceSettingsInfo! @since(version: "25.3.3")
}
@@ -0,0 +1,46 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp
*
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of DBeaver Corp and its suppliers, if any.
* The intellectual and technical concepts contained
* herein are proprietary to DBeaver Corp and its suppliers
* and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from DBeaver Corp.
*/
package io.cloudbeaver.service.ai;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.navigator.DBWFeatureProvider;
import io.cloudbeaver.utils.ServletAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.ai.utils.AIUtils;
import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import java.util.ArrayList;
import java.util.List;
public class WebAIFeatureProvider implements DBWFeatureProvider {
public static final String AI_FEATURE_ID = "ai";
private static final String FEATURE_CAN_DESCRIBE_OBJECT = "canDescribeObject";
@NotNull
@Override
public List<String> getNodeFeatures(@NotNull WebSession webSession, @NotNull DBNNode node) {
List<String> features = new ArrayList<>();
if (!ServletAppUtils.getServletApplication().getAppConfiguration().isFeatureEnabled(AI_FEATURE_ID)) {
return features;
}
if (node instanceof DBNDatabaseNode element && AIUtils.isEligible(element.getObject())) {
features.add(FEATURE_CAN_DESCRIBE_OBJECT);
}
return features;
}
}
@@ -0,0 +1,299 @@
/*
* 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.service.ai;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.ai.model.WebAIChatConversation;
import io.cloudbeaver.service.ai.model.WebAIMessage;
import io.cloudbeaver.service.ai.model.WebAISendChatMessageInfo;
import io.cloudbeaver.service.ai.model.WebAiChatResponseConsumer;
import io.cloudbeaver.service.ai.model.events.WSAiChatMessageEvent;
import io.cloudbeaver.utils.ServletAppUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.ai.*;
import org.jkiss.dbeaver.model.ai.qm.AIChatStorage;
import org.jkiss.dbeaver.model.ai.quota.UserTokenQuotaService;
import org.jkiss.dbeaver.model.ai.registry.AIAssistantRegistry;
import org.jkiss.dbeaver.model.ai.utils.AIUtils;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.navigator.DBNUtils;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;
import java.time.Clock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class WebAIUtils {
private static final Log log = Log.getLog(WebAIUtils.class);
private static final String AI_WAITING_ATTR = "ai.waiting.";
public static final String AI_CHAT_ATTR = "ai_chat";
public static final String AI_CHAT_USER_ATTR = "ai_chat_user";
@NotNull
public static List<String> convertObjectIdsToNodePaths(
@NotNull DBRProgressMonitor monitor,
@NotNull DBPProject project,
@NotNull String[] objectIds
) {
List<String> customObjectIdList = new ArrayList<>();
for (String id : objectIds) {
if (id != null && !id.isEmpty()) {
try {
DBSObject dbpObject = DBUtils.findObjectById(monitor, project, id);
if (dbpObject != null) {
DBNDatabaseNode dbNode = DBNUtils.getNodeByObject(monitor, dbpObject, false);
if (dbNode != null) {
customObjectIdList.add(dbNode.getNodeUri());
}
}
} catch (DBException e) {
log.error("Error finding object by ID: " + id, e);
}
}
}
return customObjectIdList;
}
@NotNull
public static String[] convertNodePathsToObjectIds(
@NotNull WebSession session,
@NotNull DBPProject project,
@NotNull String[] nodePaths
) {
List<DBNNode> nodes = new ArrayList<>();
for (String nodePath : nodePaths) {
if (nodePath != null && !nodePath.isEmpty()) {
try {
DBNNode node = session.getNavigatorModelOrThrow().getNodeByPath(session.getProgressMonitor(), project, nodePath);
if (node != null) {
nodes.add(node);
}
} catch (DBException e) {
log.error("Error finding object by ID: " + nodePath, e);
}
}
}
return nodes.stream()
.map(DBNDatabaseNode.class::cast)
.map(DBNDatabaseNode::getValueObject)
.map(DBSObject.class::cast)
.map(DBUtils::getObjectFullId)
.toArray(String[]::new);
}
@Nullable
public static AIChatSession findAiChatSession(@NotNull WebSession webSession) {
return webSession.getAttribute(AI_CHAT_ATTR);
}
@Nullable
public static AIContextSettingsChatConversation getConversationSettings(
@NotNull AIChatSession chatSession,
@NotNull AIChatConversation conversation
) {
DBPDataSourceContainer container = conversation.getDataSource();
if (container == null) {
return null;
}
AIContextSettingsChatConversation chatContextSettings = conversation.getCustomSettings();
if (chatContextSettings == null) {
chatContextSettings = new AIContextSettingsChatConversation(chatSession, conversation);
conversation.setCustomSettings(chatContextSettings);
}
chatContextSettings.loadDataSourceDefaults();
chatSession.updateScopeSettingsIfNeeded(chatContextSettings, container);
return chatContextSettings;
}
@NotNull
public static CompletableFuture<AIChatConversation> scheduleConversationSubmission(
@NotNull WebSession webSession,
@NotNull AIChatSession aiChatSession,
@NotNull AIChatConversation conversation,
@Nullable AIConfirmation confirmation,
@NotNull String jobName
) {
webSession.setAttribute(getWaitingAttr(conversation), true);
CompletableFuture<AIChatConversation> result = new CompletableFuture<>();
RuntimeUtils.scheduleJob(
jobName, monitor -> {
try {
AIChatResponseConsumer subscriber = new WebAiChatResponseConsumer(conversation, webSession, aiChatSession);
aiChatSession.processAICompletion(
monitor,
conversation,
subscriber,
WebAIUtils.getConversationSettings(aiChatSession, conversation),
confirmation
).whenComplete((submittedConversation, error) -> {
if (error != null) {
result.completeExceptionally(error);
} else {
result.complete(submittedConversation);
}
});
} catch (DBException e) {
log.error("Error processing AI completion", e);
var errorMessage = conversation.addMessage(AIMessage.errorMessage(e));
webSession.addSessionEvent(new WSAiChatMessageEvent(new WebAIMessage(errorMessage, conversation)));
aiChatSession.notifyMessageAdd(conversation, errorMessage);
} finally {
webSession.removeAttribute(getWaitingAttr(conversation));
}
}
);
return result;
}
@NotNull
public static String getWaitingAttr(@NotNull AIChatConversation conversation) {
return AI_WAITING_ATTR + conversation.getId();
}
@NotNull
public static AIChatSession getAiChatSession(@NotNull WebSession webSession) throws DBWebException {
validateAiPluginEnabled();
String aiChatSessionUser = webSession.getAttribute(AI_CHAT_USER_ATTR);
boolean sameUser = webSession.isAuthorizedInSecurityManager() &&
CommonUtils.equalObjects(webSession.getUserId(), aiChatSessionUser);
if (sameUser || webSession.getUserId() == null) {
AIChatSession aiChatSession = webSession.getAttribute(AI_CHAT_ATTR);
if (aiChatSession != null) {
return aiChatSession;
}
}
return createChatSession(webSession);
}
@NotNull
public static AIChatSession createChatSession(@NotNull WebSession webSession) {
AIAssistant assistant = AIAssistantRegistry.getInstance().getAssistant(webSession.getWorkspace());
AIChatStorage qmChatStorage = assistant.createChatStorage();
AIChatSession aiChatSession = new AIChatSession(
webSession.getWorkspace(),
dataSourceContainer -> DBUtils.getDefaultContext(dataSourceContainer, false),
qmChatStorage,
assistant.getChatSessionProvider(),
new UserTokenQuotaService(Clock.systemUTC(), qmChatStorage)
);
aiChatSession.addListener(new AIChatListener() {
@Override
public void messageAdded(@NotNull AIChatConversation conversation, @NotNull AIChatMessage message) {
if (message.message().getConfirmation() instanceof AIFunctionCallConfirmation fcc) {
for (AIFunctionCall fc : fcc.getFunctionCalls()) {
// functions are not resolved at the current step, we need to resolve them first
// otherwise no information about a function will be sent
AIFunctionDescriptor function = fc.getOrResolveFunction(aiChatSession.getAssistant().getToolboxManager());
if (function == null) {
log.warn("Function is not found for function call " + fc.getFunctionName());
}
}
WebAIMessage confirmationMessage = new WebAIMessage(message, conversation);
WSAiChatMessageEvent event = new WSAiChatMessageEvent(confirmationMessage);
webSession.addSessionEvent(event);
}
}
});
webSession.setAttribute(AI_CHAT_ATTR, aiChatSession);
if (webSession.getUser() != null) {
String userId = webSession.getUserId();
webSession.setAttribute(AI_CHAT_USER_ATTR, userId);
}
return aiChatSession;
}
@NotNull
public static AIChatConversation getAiChatConversation(
@NotNull WebSession webSession,
@NotNull String conversationId
) throws DBWebException {
try {
AIChatSession aiChatSession = getAiChatSession(webSession);
AIChatConversation conversation = aiChatSession.getConversation(conversationId);
if (conversation == null) {
throw new DBWebException("Invalid chat conversation ID " + conversationId);
}
return conversation;
} catch (DBException e) {
throw new DBWebException(e.getMessage(), e);
}
}
@NotNull
public static WebAISendChatMessageInfo submitPrompt(
@NotNull WebSession webSession,
@NotNull AIChatSession aiChatSession,
@NotNull AIChatConversation conversation,
@NotNull AIMessage message
) throws DBException {
if (!isMetaTransferConfirmed(conversation)) {
throw new DBWebException("AI services restricted for '%s'. Please contact your administrator if you need it.".formatted(
conversation.getDataSource()));
}
String caption = conversation.getCaption();
AIChatMessage promptMessage = conversation.addMessage(message);
webSession.addSessionEvent(new WSAiChatMessageEvent(new WebAIMessage(promptMessage, conversation)));
aiChatSession.notifyMessageAdd(conversation, promptMessage);
if (!CommonUtils.equalObjects(caption, conversation.getCaption())) {
aiChatSession.notifyConversationRenamed(conversation, conversation.getCaption());
}
if (!AIUtils.hasValidConfiguration()) {
throw new DBWebException("Invalid AI configuration");
}
if (webSession.getAttribute(WebAIUtils.getWaitingAttr(conversation)) != null) {
throw new DBWebException("Conversation is already waiting for response");
}
AIChatMessage result = new AIChatMessage(conversation.getNextMessageId(), AIMessage.assistantMessage("", null));
WebAIUtils.scheduleConversationSubmission(webSession, aiChatSession, conversation, null, "AI completion");
return new WebAISendChatMessageInfo(
new WebAIChatConversation(webSession, conversation),
new WebAIMessage(promptMessage, conversation),
new WebAIMessage(result, conversation)
);
}
private static boolean isMetaTransferConfirmed(@NotNull AIChatConversation conversation) {
DBPDataSourceContainer dataSourceContainer = conversation.getDataSource();
if (dataSourceContainer == null) {
return true;
}
AIContextSettingsDataSource dsSettings = new AIContextSettingsDataSource(dataSourceContainer);
return dsSettings.isMetaTransferConfirmed();
}
public static void validateAiPluginEnabled() throws DBWebException {
var isAiEnabled = ServletAppUtils.getServletApplication().getAppConfiguration()
.isFeatureEnabled(WebAIFeatureProvider.AI_FEATURE_ID);
if (!isAiEnabled) {
throw new DBWebException("AI feature is disabled");
}
}
}
@@ -0,0 +1,36 @@
/*
* 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.service.ai;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.service.ai.gql.WebAISettingsConfig;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.ai.AIConstants;
import org.jkiss.dbeaver.runtime.DBWorkbench;
public class WebServiceAIManager {
public WebAISettingsConfig getAiSettings() throws DBWebException {
return null;
}
@Nullable
private static String getAiLanguage() {
return DBWorkbench.getPlatform().getPreferenceStore()
.getString(AIConstants.AI_RESPONSE_LANGUAGE);
}
}
@@ -0,0 +1,176 @@
/*
* 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.service.ai.gql;
import io.cloudbeaver.*;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.WebPropertyInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.service.DBWService;
import io.cloudbeaver.service.ai.model.*;
import io.cloudbeaver.service.ai.model.inputs.DataSourceId;
import io.cloudbeaver.service.ai.model.inputs.WebAIChatConversationInput;
import io.cloudbeaver.service.ai.model.inputs.WebAIConfigurationProfileInput;
import io.cloudbeaver.service.ai.model.inputs.WebAiChatCompletionSettingsInput;
import io.cloudbeaver.service.sql.WebSQLContextInfo;
import jakarta.servlet.http.HttpServletRequest;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.rm.RMConstants;
import java.util.List;
import java.util.Map;
/**
* Service for executing GraphQL functions for AI module.
*/
public interface DBWServiceAI extends DBWService {
@NotNull
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
WebAISettingsConfig getAiSettings() throws DBWebException;
@NotNull
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
List<WebAIEngine> getEngineConfigurations() throws DBWebException;
@NotNull
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
List<WebPropertyInfo> getEngineConfigurationParameters(
@NotNull WebSession webSession,
@NotNull String engineId,
@Nullable String profileId,
@Nullable Map<String, Object> engineSettings
) throws DBWebException;
@NotNull
@WebAction
List<WebAIFunctionInfo> getFunctions(@NotNull WebSession webSession) throws DBWebException;
@NotNull
@WebAction
List<WebAIConfigurationProfile> getProfiles(@NotNull WebSession webSession) throws DBWebException;
@NotNull
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
WebAISettingsConfig saveAiSettings(
@NotNull WebSession webSession,
@NotNull WebAISettingsConfig config
) throws DBWebException;
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
boolean saveEngineConfiguration(
@NotNull WebSession webSession,
@NotNull String profileId,
@WebParameterSecure @NotNull Map<String, Object> engineSettings
) throws DBWebException;
@NotNull
@WebAction
WebAsyncTaskInfo performQueryCompletion(
@NotNull WebSession webSession,
@NotNull WebSQLContextInfo contextInfo,
@NotNull String request
) throws DBWebException;
@Nullable
@WebAction
String performQueryCompletionResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException;
@NotNull
@WebAction
WebAIChatConversation createChat(
@NotNull WebSession webSession,
@NotNull WebAIChatConversationInput conversationInput
) throws DBWebException;
@NotNull
@WebAction
WebAIChatConversation updateChatConversation(
@NotNull WebSession webSession,
@NotNull String conversationId,
@NotNull WebAIChatConversationInput conversationInput
) throws DBWebException;
@WebAction
boolean deleteChatConversation(@NotNull WebSession webSession, @NotNull String conversationId) throws DBWebException;
@NotNull
@WebAction
List<WebAIChatConversation> getChatConversations(
@NotNull WebSession webSession,
@Nullable DataSourceId dataSourceId
) throws DBWebException;
@NotNull
@WebAction
WebAIChatConversation getChatConversationInfo(
@NotNull WebSession webSession,
@NotNull String conversationId,
@Nullable Boolean loadMetrics
) throws DBWebException;
@NotNull
@WebAction
WebAISendChatMessageInfo asyncSendChatMessage(
@NotNull WebSession webSession,
@NotNull String conversationId,
@NotNull String prompt
) throws DBWebException;
@WebAction
boolean setLastChatMessage(
@NotNull WebSession webSession,
@NotNull String conversationId,
@NotNull String messageId
) throws DBWebException;
@NotNull
@WebAction
WebAIDataSourceSettings getDataSourceAiSettings(
@NotNull HttpServletRequest request,
@NotNull WebSession webSession,
@NotNull DataSourceId dataSourceId
) throws DBWebException;
@NotNull
@WebProjectAction(requireProjectPermissions = RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT)
WebAIDataSourceSettings saveDataSourceAiSettings(
@NotNull HttpServletRequest request,
@NotNull WebSession webSession,
@NotNull @WebObjectId String projectId,
@NotNull DataSourceId dataSourceId,
@NotNull WebAiChatCompletionSettingsInput settings
) throws DBWebException;
@NotNull
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
WebAIConfigurationProfile createProfile(
@NotNull WebSession webSession,
@WebParameterSecure @NotNull WebAIConfigurationProfileInput config
) throws DBWebException;
@NotNull
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
WebAIConfigurationProfile updateProfile(
@NotNull WebSession webSession,
@WebParameterSecure @NotNull WebAIConfigurationProfileInput config
) throws DBWebException;
@WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN)
boolean deleteProfile(@NotNull WebSession webSession, @NotNull String profileId) throws DBWebException;
}
@@ -0,0 +1,44 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp
*
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of DBeaver Corp and its suppliers, if any.
* The intellectual and technical concepts contained
* herein are proprietary to DBeaver Corp and its suppliers
* and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from DBeaver Corp.
*/
package io.cloudbeaver.service.ai.gql;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.ai.registry.AIEngineDescriptor;
import org.jkiss.dbeaver.model.meta.Property;
public class WebAIEngine {
@NotNull
private final AIEngineDescriptor descriptor;
public WebAIEngine(@NotNull AIEngineDescriptor descriptor) {
this.descriptor = descriptor;
}
@NotNull
@Property
public String getId() {
return descriptor.getId();
}
@NotNull
@Property
public String getName() {
return descriptor.getLabel();
}
}

Some files were not shown because too many files have changed in this diff Show More