CB-3602 fix: sql context management (#1942)

* CB-3602 fix: sql context management

* CB-3711 fix: sql context lifecycle

* CB-3602 Cleanup SQL processors/contexts on connection close

* CB-3602 Cleanup SQL processors/contexts on connection close

* CB-3602 fix: log sql script parsing errors

* CB-3602 fix: sql context info convertation

* CB-3602 fix: data concurrency loading

---------

Co-authored-by: Serge Rider <serge@jkiss.org>
Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Alexey
2023-08-29 22:53:05 +03:00
committed by GitHub
co-authored by Serge Rider EvgeniaBzzz Daria Marutkina
parent 773edfc9c0
commit f1507c3a7d
36 changed files with 221 additions and 178 deletions
@@ -25,6 +25,7 @@ import io.cloudbeaver.utils.CBModelConstants;
import io.cloudbeaver.utils.WebAppUtils;
import io.cloudbeaver.utils.WebCommonUtils;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBConstants;
import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
@@ -40,6 +41,7 @@ import org.jkiss.dbeaver.model.navigator.DBNDataSource;
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.preferences.DBPPropertySource;
import org.jkiss.dbeaver.model.rm.RMProjectPermission;
import org.jkiss.dbeaver.model.runtime.DBRRunnableParametrized;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.utils.CommonUtils;
@@ -52,6 +54,7 @@ import java.util.stream.Collectors;
*/
public class WebConnectionInfo {
private static final Log log = Log.getLog(WebConnectionInfo.class);
public static final String SECURED_VALUE = "********";
private final WebSession session;
private final DBPDataSourceContainer dataSourceContainer;
@@ -63,6 +66,7 @@ public class WebConnectionInfo {
private transient Map<String, Object> savedAuthProperties;
private transient List<WebNetworkHandlerConfigInput> savedNetworkCredentials;
private transient List<DBRRunnableParametrized<WebConnectionInfo>> closeListeners = null;
public WebConnectionInfo(WebSession session, DBPDataSourceContainer ds) {
this.session = session;
@@ -361,9 +365,23 @@ public class WebConnectionInfo {
this.savedNetworkCredentials = networkCredentials;
}
public void clearSavedCredentials() {
public void clearCache() {
this.savedAuthProperties = null;
this.savedNetworkCredentials = null;
this.fireCloseListeners();
}
public void fireCloseListeners() {
if (closeListeners != null) {
for (DBRRunnableParametrized<WebConnectionInfo> listener : closeListeners) {
try {
listener.run(this);
} catch (Exception e) {
log.debug(e);
}
}
closeListeners = null;
}
}
@Property
@@ -417,4 +435,12 @@ public class WebConnectionInfo {
return appConfig.isShowReadOnlyConnectionInfo();
}
public void addCloseListener(DBRRunnableParametrized<WebConnectionInfo> listener) {
if (closeListeners == null) {
closeListeners = new ArrayList<>();
}
closeListeners.add(listener);
}
}
@@ -62,7 +62,10 @@ import org.jkiss.dbeaver.model.runtime.AbstractJob;
import org.jkiss.dbeaver.model.runtime.BaseProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.ProxyProgressMonitor;
import org.jkiss.dbeaver.model.security.*;
import org.jkiss.dbeaver.model.security.SMAdminController;
import org.jkiss.dbeaver.model.security.SMConstants;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.model.security.SMObjectType;
import org.jkiss.dbeaver.model.security.user.SMObjectPermissions;
import org.jkiss.dbeaver.model.sql.DBQuotaException;
import org.jkiss.dbeaver.model.websocket.event.WSEventType;
@@ -593,6 +596,7 @@ public class WebSession extends BaseWebSession
}
public void removeConnection(WebConnectionInfo connectionInfo) {
connectionInfo.clearCache();
synchronized (connections) {
connections.remove(connectionInfo.getId());
}
@@ -333,7 +333,7 @@ public class WebServiceCore implements DBWServiceCore {
throw new DBWebException("Error connecting to database", e);
} finally {
dataSourceContainer.setSavePassword(oldSavePassword);
connectionInfo.clearSavedCredentials();
connectionInfo.clearCache();
}
// Mark all specified network configs as saved
boolean[] saveConfig = new boolean[1];
@@ -748,7 +748,10 @@ public class WebServiceCore implements DBWServiceCore {
@NotNull
private WebConnectionInfo closeAndDeleteConnection(
WebSession webSession, String projectId, String connectionId, boolean forceDelete
@NotNull WebSession webSession,
@NotNull String projectId,
@NotNull String connectionId,
boolean forceDelete
) throws DBWebException {
WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId);
@@ -770,7 +773,7 @@ public class WebServiceCore implements DBWServiceCore {
webSession.removeConnection(connectionInfo);
} else {
// Just reset saved credentials
connectionInfo.clearSavedCredentials();
connectionInfo.clearCache();
}
return connectionInfo;
@@ -25,7 +25,6 @@ import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.ModelPreferences;
import org.jkiss.dbeaver.model.DBPDataKind;
import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.DBUtils;
@@ -69,7 +68,7 @@ public class WebSQLProcessor implements WebSessionProvider {
private final SQLRuleManager ruleManager;
private final Map<String, WebSQLContextInfo> contexts = new LinkedHashMap<>();
private AtomicInteger contextId = new AtomicInteger();
private final AtomicInteger contextId = new AtomicInteger();
WebSQLProcessor(@NotNull WebSession webSession, @NotNull WebConnectionInfo connection) {
this.webSession = webSession;
@@ -258,6 +258,9 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> i
private static class WebSQLConfiguration {
private final Map<WebConnectionInfo, WebSQLProcessor> processors = new HashMap<>();
public WebSQLConfiguration() {
}
WebSQLProcessor getSQLProcessor(WebConnectionInfo connectionInfo) throws DBWebException {
return WebServiceBindingSQL.getSQLProcessor(connectionInfo, true);
}
@@ -277,6 +280,7 @@ public class WebServiceBindingSQL extends WebServiceBindingBase<DBWServiceSQL> i
WebSQLProcessor processor = processors.get(connectionInfo);
if (processor == null) {
processor = new WebSQLProcessor(connectionInfo.getSession(), connectionInfo);
connectionInfo.addCloseListener(processors::remove);
processors.put(connectionInfo, processor);
}
return processor;
@@ -40,10 +40,7 @@ export class AppAuthService extends Bootstrap {
resource: CachedResource<any, any, T, any, any>,
map?: (param: T | undefined) => T,
): this {
resource
.preloadResource(this.userInfoResource, () => {})
.preloadResource(this.serverConfigResource, () => {})
.before(ExecutorInterrupter.interrupter(() => !this.authenticated));
resource.preloadResource(this.userInfoResource, () => {}).before(ExecutorInterrupter.interrupter(() => !this.authenticated));
this.userInfoResource.outdateResource<T>(resource, map as any);
@@ -163,11 +163,9 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
configuration,
});
runInAction(() => {
this.resetIncludes();
this.markOutdated();
this.sessionDataResource.markOutdated();
});
this.resetIncludes();
this.setData(await this.loader());
this.sessionDataResource.markOutdated();
}
async setConfigurationParameter(key: string, value: any): Promise<UserInfo | null> {
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { ILoadableState, isLoadableStateHasException } from '@cloudbeaver/core-utils';
@@ -15,7 +15,8 @@ export interface IAutoLoadable extends ILoadableState {
load: () => void;
}
export function useAutoLoad(state: IAutoLoadable | IAutoLoadable[], enabled = true, lazy = false) {
export function useAutoLoad(component: { name: string }, state: IAutoLoadable | IAutoLoadable[], enabled = true, lazy = false) {
const [loadFunctionName] = useState(`${component.name}.useAutoLoad(...)` as const);
if (!Array.isArray(state)) {
state = [state];
}
@@ -28,18 +29,24 @@ export function useAutoLoad(state: IAutoLoadable | IAutoLoadable[], enabled = tr
}
useEffect(() => {
if (!enabled) {
return;
}
const obj = {
[loadFunctionName]: () => {
if (!enabled) {
return;
}
for (const loader of state as IAutoLoadable[]) {
if (isLoadableStateHasException(loader) || (loader.lazy === true && !lazy)) {
continue;
}
for (const loader of state as IAutoLoadable[]) {
if (isLoadableStateHasException(loader) || (loader.lazy === true && !lazy)) {
continue;
}
if (!loader.isLoaded() || loader.isOutdated?.() === true) {
loader.load();
}
}
if (!loader.isLoaded() || loader.isOutdated?.() === true) {
loader.load();
}
}
},
};
obj[loadFunctionName]();
});
}
@@ -12,14 +12,13 @@ import {
CachedMapAllKey,
CachedMapResource,
GraphQLService,
isResourceKeyList,
ResourceKey,
resourceKeyList,
ResourceKeyUtils,
SqlDialectInfo,
} from '@cloudbeaver/core-sdk';
import type { IConnectionExecutionContextInfo } from './ConnectionExecutionContext/IConnectionExecutionContextInfo';
import type { IConnectionExecutionContextInfo } from './ConnectionExecutionContext/ConnectionExecutionContextResource';
import {
ConnectionInfoActiveProjectKey,
ConnectionInfoProjectKey,
@@ -9,9 +9,8 @@ import { computed, makeObservable, observable } from 'mobx';
import type { ITask, TaskScheduler } from '@cloudbeaver/core-executor';
import type { ConnectionExecutionContextResource } from './ConnectionExecutionContextResource';
import type { ConnectionExecutionContextResource, IConnectionExecutionContextInfo } from './ConnectionExecutionContextResource';
import type { IConnectionExecutionContext } from './IConnectionExecutionContext';
import type { IConnectionExecutionContextInfo } from './IConnectionExecutionContextInfo';
export class ConnectionExecutionContext implements IConnectionExecutionContext {
get context(): IConnectionExecutionContextInfo | undefined {
@@ -7,7 +7,7 @@
*/
import { action, makeObservable, runInAction } from 'mobx';
import { AppAuthService } from '@cloudbeaver/core-authentication';
import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import {
CachedMapAllKey,
@@ -25,17 +25,21 @@ import { flat } from '@cloudbeaver/core-utils';
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource } from '../ConnectionInfoResource';
import type { IConnectionInfoParams } from '../IConnectionsResource';
import type { IConnectionExecutionContextInfo } from './IConnectionExecutionContextInfo';
export const ConnectionExecutionContextProjectKey = resourceKeyAliasFactory('@connection-folder/project', (projectId: string) => ({ projectId }));
export const NOT_INITIALIZED_CONTEXT_ID = '-1';
export type IConnectionExecutionContextInfo = SqlContextInfo & {
defaultCatalog?: string | null;
defaultSchema?: string | null;
};
@injectable()
export class ConnectionExecutionContextResource extends CachedMapResource<string, IConnectionExecutionContextInfo> {
constructor(
private readonly graphQLService: GraphQLService,
private readonly connectionInfoResource: ConnectionInfoResource,
userInfoResource: UserInfoResource,
appAuthService: AppAuthService,
) {
super();
@@ -55,6 +59,9 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
appAuthService.requireAuthentication(this);
userInfoResource.onUserChange.addHandler(() => {
this.clear();
});
connectionInfoResource.onItemUpdate.addHandler(this.updateConnectionContexts.bind(this));
connectionInfoResource.onItemDelete.addHandler(this.deleteConnectionContexts.bind(this));
@@ -128,17 +135,6 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
});
}
async refreshAll(): Promise<IConnectionExecutionContextInfo[]> {
this.resetIncludes();
await this.refresh(CachedMapAllKey);
return this.values;
}
refreshAllLazy(): void {
this.resetIncludes();
this.markOutdated(CachedMapAllKey);
}
protected async loader(originalKey: ResourceKey<string>): Promise<Map<string, IConnectionExecutionContextInfo>> {
const contextsList: IConnectionExecutionContextInfo[] = [];
let projectId: string | undefined;
@@ -218,7 +214,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource<string
}
}
function getBaseContext(context: SqlContextInfo): IConnectionExecutionContextInfo {
function getBaseContext(context: IConnectionExecutionContextInfo): IConnectionExecutionContextInfo {
return {
...context,
};
@@ -7,7 +7,7 @@
*/
import type { ITask } from '@cloudbeaver/core-executor';
import type { IConnectionExecutionContextInfo } from './IConnectionExecutionContextInfo';
import type { IConnectionExecutionContextInfo } from './ConnectionExecutionContextResource';
export interface IConnectionExecutionContext {
readonly context: IConnectionExecutionContextInfo | undefined;
@@ -1,13 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { SqlContextInfo } from '@cloudbeaver/core-sdk';
export type IConnectionExecutionContextInfo = SqlContextInfo & {
defaultCatalog?: string | null;
defaultSchema?: string | null;
};
@@ -7,7 +7,7 @@
*/
import { action, makeObservable, observable, runInAction } from 'mobx';
import { AppAuthService } from '@cloudbeaver/core-authentication';
import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication';
import { injectable } from '@cloudbeaver/core-di';
import { ExecutorInterrupter, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects';
@@ -88,6 +88,7 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
sessionDataResource: SessionDataResource,
appAuthService: AppAuthService,
connectionInfoEventHandler: ConnectionInfoEventHandler,
userInfoResource: UserInfoResource,
) {
super();
@@ -109,6 +110,9 @@ export class ConnectionInfoResource extends CachedMapResource<IConnectionInfoPar
this.onItemDelete.addHandler(ExecutorInterrupter.interrupter(() => this.sessionUpdate));
this.onConnectionCreate.addHandler(ExecutorInterrupter.interrupter(() => this.sessionUpdate));
userInfoResource.onUserChange.addHandler(() => {
this.clear();
});
appAuthService.requireAuthentication(this);
this.sync(
this.projectInfoResource,
@@ -2,7 +2,6 @@ export * from './ConnectionExecutionContext/ConnectionExecutionContext';
export * from './ConnectionExecutionContext/ConnectionExecutionContextResource';
export * from './ConnectionExecutionContext/ConnectionExecutionContextService';
export * from './ConnectionExecutionContext/IConnectionExecutionContext';
export * from './ConnectionExecutionContext/IConnectionExecutionContextInfo';
export * from './extensions/IObjectCatalogProvider';
export * from './extensions/IObjectCatalogSetter';
@@ -49,7 +49,7 @@ export const ContextMenu = observer<IContextMenuProps, HTMLButtonElement>(
const menu = useRef<IMenuState>();
useAutoLoad(menuData.loaders, !lazy, menuVisible);
useAutoLoad({ name: `${ContextMenu.name}(${menuData.menu.id})` }, menuData.loaders, !lazy, menuVisible);
const handlers = useObjectRef(
() => ({
@@ -51,7 +51,7 @@ export const MenuBar = observer<IMenuBarProps, HTMLDivElement>(
forwardRef(function MenuBar({ menu, nestedMenuSettings, rtl, className, ...props }, ref) {
const styles = useS(style);
const items = menu.items;
useAutoLoad(menu.loaders);
useAutoLoad(MenuBar, menu.loaders);
if (!items.length) {
return null;
@@ -32,7 +32,7 @@ export const SubMenuElement = observer<ISubMenuElementProps, HTMLButtonElement>(
const handler = subMenuData.handler;
const hidden = getComputed(() => handler?.isHidden?.(subMenuData.context));
useAutoLoad(subMenuData.loaders, !hidden, visible);
useAutoLoad(SubMenuElement, subMenuData.loaders, !hidden, visible);
const handlers = useObjectRef(
() => ({
@@ -70,7 +70,7 @@ export const GrantedConnections: TabContainerPanelComponent<ITeamFormProps> = ob
const grantedConnections = getComputed(() => connections.filter(connection => state.state.grantedSubjects.includes(connection.id)));
useAutoLoad(state, selected && !loaded);
useAutoLoad(GrantedConnections, state, selected && !loaded);
if (!selected) {
return null;
@@ -61,7 +61,7 @@ export const GrantedUsers: TabContainerPanelComponent<ITeamFormProps> = observer
users.data.filter<AdminUser>((user): user is AdminUser => !!user && state.state.grantedUsers.includes(user.userId)),
);
useAutoLoad(state, selected && !state.state.loaded);
useAutoLoad(GrantedUsers, state, selected && !state.state.loaded);
if (!selected) {
return null;
@@ -114,7 +114,7 @@ export const OriginInfoPanel: TabContainerPanelComponent<IUserFormProps> = obser
const { selected } = useTab(tabId);
useAutoLoad(loadableState, selected);
useAutoLoad(OriginInfoPanel, loadableState, selected);
if (!selected) {
return null;
@@ -54,7 +54,7 @@ export const ConnectionAccess: TabContainerPanelComponent<IConnectionFormProps>
const { selected } = useTab(tabId);
useAutoLoad(state, selected);
useAutoLoad(ConnectionAccess, state, selected);
const users = useResource(ConnectionAccess, UsersResource, CachedResourcePageListKey(0, 1000).setTarget(UsersResourceFilterKey()), {
active: selected,
@@ -91,9 +91,10 @@ export function useGroupingDataModel(
if (columns.length !== 0 && functions.length !== 0 && sourceResultId) {
const executionContext = sourceModel.source.executionContext;
model.model.source.setExecutionContext(executionContext).setSupportedDataFormats(connectionInfo?.supportedDataFormats ?? []);
const context = executionContext?.context;
if (executionContext?.context) {
const connectionKey = createConnectionParam(executionContext.context.projectId, executionContext.context.connectionId);
if (context) {
const connectionKey = createConnectionParam(context.projectId, context.connectionId);
model.model
.setOptions({
@@ -79,6 +79,7 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
}
const executionContext = await this.ensureContextCreated();
const context = executionContext.context!;
const offset = this.offset;
const limit = this.count;
@@ -86,8 +87,8 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
if (
prevResults.length === 1 &&
prevResults[0].contextId === executionContext.context!.id &&
prevResults[0].connectionId === executionContext.context?.connectionId &&
prevResults[0].contextId === context.id &&
prevResults[0].connectionId === context.connectionId &&
prevResults[0].id !== null
) {
firstResultId = prevResults[0].id;
@@ -95,8 +96,8 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
const task = this.asyncTaskInfoService.create(async () => {
const { taskInfo } = await this.graphQLService.sdk.asyncReadDataFromContainer({
connectionId: executionContext.context!.connectionId,
contextId: executionContext.context!.id,
connectionId: context.connectionId,
contextId: context.id,
containerNodePath: options.containerNodePath,
resultId: firstResultId,
filter: {
@@ -140,7 +140,7 @@ export class NavigationTabsService extends View<ITab> {
currentTab: computed,
currentTabId: computed,
tabIdList: computed<string[]>({
equals: isArraysEqual,
equals: (a, b) => isArraysEqual(a, b, undefined, true),
}),
openTab: action,
selectTab: action,
@@ -32,7 +32,7 @@ export const NavigationTreeFiltersDialog = observer<DialogComponentProps<Payload
const navTreeResource = useService(NavTreeResource);
const state = useFilters(payload.node.id);
useAutoLoad(state);
useAutoLoad(NavigationTreeFiltersDialog, state);
async function submit() {
try {
@@ -17,7 +17,7 @@ import { TaskScheduler } from '@cloudbeaver/core-executor';
import type { ProjectInfoResource } from '@cloudbeaver/core-projects';
import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-resource-manager';
import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { debounce, getPathName, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { debounce, getPathName, isArraysEqual, isNotNullDefined, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
@@ -75,10 +75,8 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
get executionContext(): IConnectionExecutionContextInfo | undefined {
if (
this.state.executionContext &&
!this.connectionInfoResource.has(createConnectionParam(this.state.executionContext.projectId, this.state.executionContext.connectionId))
) {
const executionContext = this.state?.executionContext;
if (!executionContext || !this.connectionInfoResource.has(createConnectionParam(executionContext.projectId, executionContext.connectionId))) {
return undefined;
}
return this.state.executionContext;
@@ -228,9 +226,13 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
}
setExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
executionContext = JSON.parse(JSON.stringify(toJS(executionContext) ?? {}));
if (executionContext) {
executionContext = JSON.parse(JSON.stringify(toJS(executionContext) ?? {}));
}
if (this.resourceKey && executionContext?.projectId && getRmResourceKey(this.resourceKey).projectId !== executionContext.projectId) {
const projectId = executionContext?.projectId;
if (this.resourceKey && isNotNullDefined(projectId) && getRmResourceKey(this.resourceKey).projectId !== projectId) {
throw new Error('Resource SQL Data Source and Execution context projects don\t match');
}
@@ -124,6 +124,7 @@ export class SqlEditorBootstrap extends Bootstrap {
case ACTION_RENAME: {
const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE);
const dataSource = this.sqlDataSourceService.get(state.editorId);
const executionContext = dataSource?.executionContext;
if (!dataSource) {
return;
@@ -131,11 +132,8 @@ export class SqlEditorBootstrap extends Bootstrap {
let connection: Connection | undefined;
if (dataSource.executionContext) {
connection = this.connectionInfoResource.get({
projectId: dataSource.executionContext.projectId,
connectionId: dataSource.executionContext.connectionId,
});
if (executionContext) {
connection = this.connectionInfoResource.get(createConnectionParam(executionContext.projectId, executionContext.connectionId));
}
const name = getSqlEditorName(state, dataSource, connection);
@@ -101,13 +101,14 @@ export class SqlEditorNavigatorService {
tab = this.navigationTabsService.findTab(
isSQLEditorTab(tab => {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
return (
tab.handlerState.source === SQL_EDITOR_SOURCE_ACTION &&
dataSource?.executionContext !== undefined &&
executionContext !== undefined &&
data.connectionKey !== undefined &&
this.connectionInfoResource.isKeyEqual(
createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId),
createConnectionParam(executionContext.projectId, executionContext.connectionId),
data.connectionKey,
)
);
@@ -10,7 +10,7 @@ import { useContext } from 'react';
import styled from 'reshadow';
import { s, useStyles } from '@cloudbeaver/core-blocks';
import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { Connection, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui';
import { CaptureViewContext, useDataContext } from '@cloudbeaver/core-view';
@@ -32,12 +32,10 @@ export const SqlEditorTab: TabHandlerTabComponent<ISqlEditorTabState> = observer
const dataSource = sqlDataSourceService.get(tab.handlerState.editorId);
let connection: Connection | undefined;
const executionContext = dataSource?.executionContext;
if (dataSource?.executionContext) {
connection = connectionInfo.get({
projectId: dataSource.executionContext.projectId,
connectionId: dataSource.executionContext.connectionId,
});
if (executionContext) {
connection = connectionInfo.get(createConnectionParam(executionContext.projectId, executionContext.connectionId));
}
const name = getSqlEditorName(tab.handlerState, dataSource, connection);
@@ -155,9 +155,10 @@ export class SqlEditorTabService extends Bootstrap {
for (const tab of tabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
if (dataSource?.executionContext) {
const contextConnection = createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId);
if (executionContext) {
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (this.connectionInfoResource.isIntersect(key, contextConnection)) {
this.resetConnectionInfo(tab);
@@ -168,12 +169,13 @@ export class SqlEditorTabService extends Bootstrap {
private getNavNode(tab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
if (!dataSource?.executionContext) {
if (!executionContext) {
return;
}
const { projectId, connectionId, defaultCatalog, defaultSchema } = dataSource.executionContext;
const { projectId, connectionId, defaultCatalog, defaultSchema } = executionContext;
const connectionKey = createConnectionParam(projectId, connectionId);
const connection = this.connectionInfoResource.get(connectionKey);
@@ -222,19 +224,20 @@ export class SqlEditorTabService extends Bootstrap {
for (const tab of tabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId)!;
const executionContext = this.connectionExecutionContextService.get(dataSource.executionContext!.id);
const executionContext = this.connectionExecutionContextService.get(dataSource.executionContext!.id)?.context;
if (!executionContext?.context) {
if (dataSource.executionContext) {
const contextConnection = createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId);
if (!executionContext) {
const executionContext = dataSource.executionContext;
if (executionContext) {
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (!this.connectionInfoResource.has(contextConnection)) {
this.resetConnectionInfo(tab);
}
}
} else {
dataSource.setExecutionContext({ ...executionContext.context });
this.attachToProject(tab, executionContext.context.projectId);
dataSource.setExecutionContext({ ...executionContext });
this.attachToProject(tab, executionContext.projectId);
}
}
}
@@ -250,11 +253,12 @@ export class SqlEditorTabService extends Bootstrap {
for (const tab of tabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId)!;
const executionContext = dataSource.executionContext;
if (dataSource.executionContext) {
const contextConnection = createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId);
if (executionContext) {
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (ResourceKeyUtils.isIntersect(key, dataSource.executionContext!.id) && !this.connectionInfoResource.has(contextConnection)) {
if (ResourceKeyUtils.isIntersect(key, executionContext.id) && !this.connectionInfoResource.has(contextConnection)) {
this.resetConnectionInfo(tab);
}
}
@@ -286,11 +290,12 @@ export class SqlEditorTabService extends Bootstrap {
}
const dataSource = this.sqlDataSourceService.create(tab.handlerState, tab.handlerState.datasourceKey);
const executionContext = dataSource.executionContext;
if (dataSource.executionContext) {
if (executionContext) {
await this.connectionInfoResource.load(ConnectionInfoActiveProjectKey);
const contextConnection = createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId);
const contextConnection = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (!this.connectionInfoResource.has(contextConnection)) {
this.resetConnectionInfo(tab);
@@ -321,13 +326,13 @@ export class SqlEditorTabService extends Bootstrap {
}
private getConnectionId(tab: ITab<ISqlEditorTabState>): IConnectionInfoParams | undefined {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const context = this.sqlDataSourceService.get(tab.handlerState.editorId)?.executionContext;
if (!dataSource?.executionContext) {
if (!context) {
return undefined;
}
return createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId);
return createConnectionParam(context.projectId, context.connectionId);
}
private getObjectCatalogId(tab: ITab<ISqlEditorTabState>) {
@@ -422,12 +427,13 @@ export class SqlEditorTabService extends Bootstrap {
if (data.state === 'before') {
for (const tab of this.sqlEditorTabs) {
const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId);
const executionContext = dataSource?.executionContext;
if (!dataSource?.executionContext) {
if (!executionContext) {
continue;
}
const connectionKey = createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId);
const connectionKey = createConnectionParam(executionContext.projectId, executionContext.connectionId);
if (!this.connectionInfoResource.isIntersect(connectionsKey, connectionKey)) {
continue;
@@ -481,12 +487,6 @@ export class SqlEditorTabService extends Bootstrap {
}
private async handleTabUnload(editorTab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.executionContext) {
await this.sqlEditorService.destroyContext(dataSource.executionContext);
}
await this.sqlDataSourceService.unload(editorTab.handlerState.editorId);
this.sqlResultTabsService.removeResultTabs(editorTab.handlerState);
@@ -504,6 +504,11 @@ export class SqlEditorTabService extends Bootstrap {
}
private async handleTabClose(editorTab: ITab<ISqlEditorTabState>) {
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
if (dataSource?.executionContext) {
await this.sqlEditorService.destroyContext(dataSource.executionContext);
}
await this.sqlDataSourceService.destroy(editorTab.handlerState.editorId);
}
}
@@ -12,6 +12,7 @@ import { useExecutor, useObservableRef } from '@cloudbeaver/core-blocks';
import { ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { SyncExecutor } from '@cloudbeaver/core-executor';
import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk';
import { createLastPromiseGetter, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils';
@@ -34,6 +35,7 @@ interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly connectionExecutionContextService: ConnectionExecutionContextService;
readonly sqlQueryService: SqlQueryService;
readonly sqlEditorService: SqlEditorService;
readonly notificationService: NotificationService;
readonly sqlExecutionPlanService: SqlExecutionPlanService;
readonly commonDialogService: CommonDialogService;
readonly sqlResultTabsService: SqlResultTabsService;
@@ -61,6 +63,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
const sqlQueryService = useService(SqlQueryService);
const sqlDialectInfoService = useService(SqlDialectInfoService);
const sqlEditorService = useService(SqlEditorService);
const notificationService = useService(NotificationService);
const sqlExecutionPlanService = useService(SqlExecutionPlanService);
const sqlResultTabsService = useService(SqlResultTabsService);
const commonDialogService = useService(CommonDialogService);
@@ -72,13 +75,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
return sqlDataSourceService.get(this.state.editorId);
},
get dialect(): SqlDialectInfo | undefined {
if (!this.dataSource?.executionContext) {
const executionContext = this.dataSource?.executionContext;
if (!executionContext) {
return undefined;
}
return this.sqlDialectInfoService.getDialectInfo(
createConnectionParam(this.dataSource.executionContext.projectId, this.dataSource.executionContext.connectionId),
);
return this.sqlDialectInfoService.getDialectInfo(createConnectionParam(executionContext.projectId, executionContext.connectionId));
},
get activeSegmentMode(): ISQLEditorMode {
@@ -155,15 +157,17 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
this.parser.setScript(this.value);
this.reactionDisposer = autorun(() => {
if (this.dataSource?.executionContext?.id) {
const context = this.connectionExecutionContextService.get(this.dataSource.executionContext.id);
const executionContext = this.dataSource?.executionContext;
if (executionContext) {
const context = this.connectionExecutionContextService.get(executionContext.id)?.context;
if (context) {
const key = createConnectionParam(this.dataSource.executionContext.projectId, this.dataSource.executionContext.connectionId);
const key = createConnectionParam(context.projectId, context.connectionId);
untracked(() => {
this.sqlDialectInfoService.loadSqlDialectInfo(key).then(async dialect => {
await this.updateParserScriptsThrottle();
this.sqlDialectInfoService.loadSqlDialectInfo(key).then(async () => {
try {
await this.updateParserScriptsThrottle();
} catch {}
});
});
}
@@ -187,13 +191,19 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
parseScript: createLastPromiseGetter(),
getHintProposals: throttleAsync(async function getHintProposals(this: ISQLEditorDataPrivate, position, simple) {
if (!this.dataSource?.executionContext) {
const executionContext = this.dataSource?.executionContext;
if (!executionContext) {
return [];
}
const { connectionId, id } = this.dataSource.executionContext;
const hints = await this.sqlEditorService.getAutocomplete(connectionId, id, this.value, position, MAX_HINTS_LIMIT, simple);
const hints = await this.sqlEditorService.getAutocomplete(
executionContext.connectionId,
executionContext.id,
this.value,
position,
MAX_HINTS_LIMIT,
simple,
);
this.hintsLimitIsMet = hints.length >= MAX_HINTS_LIMIT;
@@ -233,9 +243,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
}
const query = this.getSubQuery();
await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query =>
this.sqlQueryService.executeEditorQuery(this.state, query.query, false),
);
try {
await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query =>
this.sqlQueryService.executeEditorQuery(this.state, query.query, false),
);
} catch {}
},
async loadDatabaseDataModels(): Promise<void> {
@@ -262,9 +274,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
}
const query = this.getSubQuery();
await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query =>
this.sqlQueryService.executeEditorQuery(this.state, query.query, true),
);
try {
await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query =>
this.sqlQueryService.executeEditorQuery(this.state, query.query, true),
);
} catch {}
},
async showExecutionPlan(): Promise<void> {
@@ -277,9 +291,11 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
const query = this.getSubQuery();
await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query =>
this.sqlExecutionPlanService.executeExecutionPlan(this.state, query.query),
);
try {
await this.executeQueryAction(await this.executeQueryAction(query, () => this.getResolvedSegment()), query =>
this.sqlExecutionPlanService.executeExecutionPlan(this.state, query.query),
);
} catch {}
},
async switchEditing(): Promise<void> {
@@ -363,7 +379,14 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
return;
}
const { queries } = await this.parseScript([connectionId, script], () => this.sqlEditorService.parseSQLScript(connectionId, script));
const { queries } = await this.parseScript([connectionId, script], async () => {
try {
return await this.sqlEditorService.parseSQLScript(connectionId, script);
} catch (exception: any) {
this.notificationService.logException(exception, 'Failed to parse SQL script');
throw exception;
}
});
if (this.parser.actualScript === script) {
this.parser.setQueries(queries);
@@ -462,6 +485,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
sqlEditorService,
sqlExecutionPlanService,
sqlResultTabsService,
notificationService,
commonDialogService,
},
);
@@ -8,7 +8,7 @@
import { action } from 'mobx';
import { useObservableRef } from '@cloudbeaver/core-blocks';
import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { Connection, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -101,14 +101,11 @@ export function useTools(state: ISqlEditorTabState): Readonly<State> {
});
const dataSource = sqlDataSourceService.get(this.state.editorId);
const executionContext = dataSource?.executionContext;
let connection: Connection | undefined;
if (dataSource?.executionContext) {
connection = this.connectionInfoResource.get({
projectId: dataSource.executionContext.projectId,
connectionId: dataSource.executionContext.connectionId,
});
if (executionContext) {
connection = this.connectionInfoResource.get(createConnectionParam(executionContext.projectId, executionContext.connectionId));
}
const name = getSqlEditorName(this.state, dataSource, connection);
@@ -12,7 +12,6 @@ import styled, { css } from 'reshadow';
import {
Button,
getComputed,
Loader,
Overlay,
OverlayActions,
OverlayHeader,
@@ -20,10 +19,7 @@ import {
OverlayHeaderSubTitle,
OverlayHeaderTitle,
OverlayMessage,
useExecutor,
useResource,
useSplitUserState,
useStyles,
useTranslate,
} from '@cloudbeaver/core-blocks';
import {
@@ -39,7 +35,6 @@ import { NodeManagerUtils } from '@cloudbeaver/core-navigation-tree';
import type { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService';
import { SqlEditorService } from './SqlEditorService';
import { useDataSource } from './useDataSource';
const viewerStyles = css`
OverlayActions {
@@ -57,12 +52,12 @@ export const SqlEditorOverlay = observer<Props>(function SqlEditorOverlay({ stat
const sqlDataSourceService = useService(SqlDataSourceService);
const dataSource = sqlDataSourceService.get(state.editorId);
const executionContextId = dataSource?.executionContext?.id;
const executionContext = dataSource?.executionContext;
useDataSource(dataSource);
const connection = useResource(
SqlEditorOverlay,
ConnectionInfoResource,
dataSource?.executionContext ? createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId) : null,
executionContext ? createConnectionParam(executionContext.projectId, executionContext.connectionId) : null,
);
const driver = useResource(SqlEditorOverlay, DBDriverResource, connection.tryGetData?.driverId ?? null);
@@ -118,13 +118,11 @@ export class SqlEditorService {
getName(tabState: ISqlEditorTabState): string {
const dataSource = this.sqlDataSourceService.get(tabState.editorId);
const executionContext = dataSource?.executionContext;
let connection: Connection | undefined;
if (dataSource?.executionContext) {
connection = this.connectionInfoResource.get({
projectId: dataSource.executionContext.projectId,
connectionId: dataSource.executionContext.connectionId,
});
if (executionContext) {
connection = this.connectionInfoResource.get(createConnectionParam(executionContext.projectId, executionContext.connectionId));
}
return getSqlEditorName(tabState, dataSource, connection);
@@ -186,21 +184,22 @@ export class SqlEditorService {
return this.sqlDataSourceService.executeAction(
state.editorId,
async dataSource => {
if (!dataSource.executionContext) {
const executionContext = dataSource?.executionContext;
if (!executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
await this.connectionExecutionContextResource.load(ConnectionExecutionContextProjectKey(dataSource.executionContext.projectId));
await this.connectionExecutionContextResource.load(ConnectionExecutionContextProjectKey(executionContext.projectId));
if (this.connectionExecutionContextResource.has(dataSource.executionContext.id)) {
return this.connectionExecutionContextService.get(dataSource.executionContext.id);
if (this.connectionExecutionContextResource.has(executionContext.id)) {
return this.connectionExecutionContextService.get(executionContext.id);
}
const context = await this.initContext(
createConnectionParam(dataSource.executionContext.projectId, dataSource.executionContext.connectionId),
dataSource.executionContext.defaultCatalog,
dataSource.executionContext.defaultSchema,
createConnectionParam(executionContext.projectId, executionContext.connectionId),
executionContext.defaultCatalog,
executionContext.defaultSchema,
);
if (!context?.context) {
@@ -35,7 +35,7 @@ export class ScriptPreviewService {
}
private async tryGetScript(model: IDatabaseDataModel, resultIndex: number): Promise<string> {
const executionContext = model.source.executionContext;
const executionContext = model.source.executionContext?.context;
if (!executionContext) {
throw new Error('Execution context is not provided');
@@ -48,9 +48,9 @@ export class ScriptPreviewService {
}
const updateVariables: UpdateResultsDataBatchScriptMutationVariables = {
projectId: executionContext.context!.projectId,
connectionId: executionContext.context!.connectionId,
contextId: executionContext.context!.id,
projectId: executionContext.projectId,
connectionId: executionContext.connectionId,
contextId: executionContext.id,
resultsId: result.id,
};
let editor: ResultSetEditAction | DocumentEditAction | undefined;