From 6c95b65dd6681e4d77f97dce4eba5108ce6ca6fd Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 29 Apr 2026 17:00:20 +0200 Subject: [PATCH 1/6] dbeaver/pro#8981 add api for reference panel in cloudbeaver --- .../schema/service.sql.graphqls | 21 +++++ .../service/sql/DBWServiceSQL.java | 11 +++ .../service/sql/WebSQLQueryResultColumn.java | 21 +++++ .../sql/WebSQLQueryResultColumnReference.java | 59 +++++++++++++ .../service/sql/WebServiceBindingSQL.java | 11 +++ .../service/sql/impl/WebServiceSQL.java | 86 ++++++++++++++++++- 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 3155e9175ed..5d2824d7ddd 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -72,6 +72,11 @@ input SQLDataFilter { orderBy: String } +type SQLResultColumnReference { + associationName: String! + targetEntityName: String +} + type SQLResultColumn { position: Int! name: String @@ -96,6 +101,9 @@ type SQLResultColumn { "Operations supported for this attribute" supportedOperations: [DataTypeLogicalOperation!]! + "Foreign key references available for this column" + references: [SQLResultColumnReference!]! + "Description of the column" description: String @since(version: "25.1.3") } @@ -451,6 +459,19 @@ extend type Mutation { dataFormat: ResultDataFormat ): AsyncTaskInfo! + "Creates async task for reading referenced data by foreign key cell" + asyncSqlNavigateForeignKey( + projectId: ID, + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + columnIndex: Int!, + row: SQLResultRow!, + associationName: String, + resultId: ID, + dataFormat: ResultDataFormat + ): AsyncTaskInfo! + "Returns transaction log info for the specified project, connection and context" getTransactionLogInfo( projectId: ID!, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 42b552084d9..6450a5d488e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -126,6 +126,17 @@ WebAsyncTaskInfo asyncReadDataFromContainer( @Nullable WebSQLDataFilter filter, @Nullable WebDataFormat dataFormat) throws DBWebException; + @WebAction + WebAsyncTaskInfo asyncNavigateForeignKey( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer columnIndex, + @NotNull WebSQLResultsRow row, + @Nullable String associationName, + @Nullable String resultId, + @Nullable WebDataFormat dataFormat) throws DBException; + /** * Reads dynamic trace from provided database results. */ diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java index a3a5778b3cf..c0bf3dfab52 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java @@ -24,6 +24,12 @@ import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.struct.DBSEntityAssociation; +import org.jkiss.dbeaver.model.struct.DBSEntityReferrer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Web SQL query resultset. @@ -130,6 +136,21 @@ public DBCLogicalOperator[] getSupportedOperations() { return attrMeta.getValueHandler().getSupportedOperators(attrMeta); } + @Property + public List getReferences() { + List referrers = attrMeta.getReferrers(); + if (referrers == null || referrers.isEmpty()) { + return Collections.emptyList(); + } + List references = new ArrayList<>(); + for (DBSEntityReferrer referrer : referrers) { + if (referrer instanceof DBSEntityAssociation) { + references.add(new WebSQLQueryResultColumnReference((DBSEntityAssociation) referrer)); + } + } + return references; + } + @Override public String toString() { return attrMeta.getName(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java new file mode 100644 index 00000000000..0a257683273 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java @@ -0,0 +1,59 @@ +/* + * 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.code.Nullable; +import org.jkiss.dbeaver.model.DBPEvaluationContext; +import org.jkiss.dbeaver.model.DBUtils; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.struct.DBSEntityAssociation; +import org.jkiss.dbeaver.model.struct.DBSEntityConstraint; + +/** + * Web SQL query result column reference. + */ +public class WebSQLQueryResultColumnReference { + + @NotNull + private final DBSEntityAssociation association; + + public WebSQLQueryResultColumnReference(@NotNull DBSEntityAssociation association) { + this.association = association; + } + + @NotNull + @Property + public String getAssociationName() { + return association.getName(); + } + + @Nullable + @Property + public String getTargetEntityName() { + DBSEntityConstraint referencedConstraint = association.getReferencedConstraint(); + if (referencedConstraint == null) { + return null; + } + DBSEntity targetEntity = referencedConstraint.getParentObject(); + if (targetEntity == null) { + return null; + } + return DBUtils.getObjectFullName(targetEntity, DBPEvaluationContext.UI); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index bde7777ba27..ccdf5321b1a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -232,6 +232,17 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getDataFilter(env), getDataFormat(env) )) + .dataFetcher("asyncSqlNavigateForeignKey", env -> + getService(env).asyncNavigateForeignKey( + getWebSession(env), + getSQLContext(env), + getArgumentVal(env, "resultsId"), + getArgumentVal(env, "columnIndex"), + new WebSQLResultsRow(getArgument(env, "row")), + getArgument(env, "associationName"), + getArgument(env, "resultId"), + getDataFormat(env) + )) .dataFetcher("asyncSqlExecuteResults", env -> getService(env).asyncGetQueryResults( getWebSession(env), getArgumentVal(env, "taskId") diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 5ff65671c61..c0598e1486b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -36,6 +36,8 @@ 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; @@ -59,9 +61,7 @@ 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.DBSDataContainer; -import org.jkiss.dbeaver.model.struct.DBSObject; -import org.jkiss.dbeaver.model.struct.DBSWrapper; +import org.jkiss.dbeaver.model.struct.*; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -603,6 +603,86 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { return contextInfo.getProcessor().getWebSession().createAndRunAsyncTask("Read data from container " + nodePath, runnable); } + @Override + public WebAsyncTaskInfo asyncNavigateForeignKey( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull Integer columnIndex, + @NotNull WebSQLResultsRow row, + @Nullable String associationName, + @Nullable String resultId, + @Nullable WebDataFormat dataFormat + ) { + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException { + try { + monitor.beginTask("Navigate foreign key", 1); + //Get bindings + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + DBDAttributeBinding[] attributes = resultsInfo.getAttributes(); + if (columnIndex < 0 || columnIndex >= attributes.length) { + throw new DBWebException("Column index '" + columnIndex + "' is out of range"); + } + DBDAttributeBinding attribute = attributes[columnIndex]; + // Get association + List referrers = attribute.getReferrers(); + if (CommonUtils.isEmpty(referrers)) { + throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); + } + DBSEntityAssociation association = null; + for (DBSEntityReferrer referrer : referrers) { + if (referrer instanceof DBSEntityAssociation) { + DBSEntityAssociation referrerAssociation = (DBSEntityAssociation) referrer; + if (CommonUtils.isEmpty(associationName) || + CommonUtils.equalObjects(associationName, referrerAssociation.getName())) { + association = referrerAssociation; + break; + } + } + } + if (association == null) { + if (CommonUtils.isEmpty(associationName)) { + throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); + } + throw new DBException("Association '" + associationName + "' not found in attribute [" + attribute.getName() + "]"); + } + + WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider( + resultsId, + contextInfo, + List.of(row) + ); + DBDReferenceNavigation navigation = 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, + resultId, + 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 @Override public List readDynamicTrace( From 1f7a91315b8f86ae656781ad61ed9e76beef86c2 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Thu, 30 Apr 2026 15:42:02 +0200 Subject: [PATCH 2/6] dbeaver/pro#8981 fixes --- .../bundles/io.cloudbeaver.server/schema/service.sql.graphqls | 1 - .../src/io/cloudbeaver/service/sql/DBWServiceSQL.java | 1 - .../src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java | 1 - .../src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java | 3 +-- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 5d2824d7ddd..0b70ecb348a 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -468,7 +468,6 @@ extend type Mutation { columnIndex: Int!, row: SQLResultRow!, associationName: String, - resultId: ID, dataFormat: ResultDataFormat ): AsyncTaskInfo! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 6450a5d488e..d4ec2001563 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -134,7 +134,6 @@ WebAsyncTaskInfo asyncNavigateForeignKey( @NotNull Integer columnIndex, @NotNull WebSQLResultsRow row, @Nullable String associationName, - @Nullable String resultId, @Nullable WebDataFormat dataFormat) throws DBException; /** diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index ccdf5321b1a..2c9972840d4 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -240,7 +240,6 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getArgumentVal(env, "columnIndex"), new WebSQLResultsRow(getArgument(env, "row")), getArgument(env, "associationName"), - getArgument(env, "resultId"), getDataFormat(env) )) .dataFetcher("asyncSqlExecuteResults", env -> diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index c0598e1486b..b6409e7efc1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -611,7 +611,6 @@ public WebAsyncTaskInfo asyncNavigateForeignKey( @NotNull Integer columnIndex, @NotNull WebSQLResultsRow row, @Nullable String associationName, - @Nullable String resultId, @Nullable WebDataFormat dataFormat ) { WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { @@ -667,7 +666,7 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { contextInfo, monitor, targetDataContainer, - resultId, + null, WebSQLDataFilter.from(navigation.getTargetFilter()), dataFormat ); From 289781e99be49e4ff2b89ceadd5f6b5aeaf0ba6b Mon Sep 17 00:00:00 2001 From: Ruslan Date: Tue, 26 May 2026 12:31:56 +0200 Subject: [PATCH 3/6] dbeaver/pro#8981 api ref panel --- .../schema/service.sql.graphqls | 9 ++- .../service/sql/DBWServiceSQL.java | 3 +- .../service/sql/WebSQLQueryResultColumn.java | 64 ++++++++++++--- .../sql/WebSQLQueryResultColumnReference.java | 26 ++++-- .../service/sql/WebServiceBindingSQL.java | 3 +- .../service/sql/impl/WebServiceSQL.java | 81 ++++++++++++------- 6 files changed, 137 insertions(+), 49 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 0b70ecb348a..1667965c7a3 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -75,6 +75,8 @@ input SQLDataFilter { type SQLResultColumnReference { associationName: String! targetEntityName: String + "True if this is a reverse reference (another entity's FK points to this column); false for forward foreign keys" + isReference: Boolean! @since(version: "26.1.0") } type SQLResultColumn { @@ -101,7 +103,7 @@ type SQLResultColumn { "Operations supported for this attribute" supportedOperations: [DataTypeLogicalOperation!]! - "Foreign key references available for this column" + "Foreign key references for this column, plus reverse references from other tables that point to it (distinguish via SQLResultColumnReference.isReference)" references: [SQLResultColumnReference!]! "Description of the column" @@ -459,7 +461,7 @@ extend type Mutation { dataFormat: ResultDataFormat ): AsyncTaskInfo! - "Creates async task for reading referenced data by foreign key cell" + "Creates async task for reading referenced data by foreign key cell. Set isReference=true to navigate a reverse reference (other entity's FK pointing to this row)." asyncSqlNavigateForeignKey( projectId: ID, connectionId: ID!, @@ -468,7 +470,8 @@ extend type Mutation { columnIndex: Int!, row: SQLResultRow!, associationName: String, - dataFormat: ResultDataFormat + dataFormat: ResultDataFormat, + isReference: Boolean @since(version: "26.1.0") ): AsyncTaskInfo! "Returns transaction log info for the specified project, connection and context" diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index d4ec2001563..968dce91023 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -134,7 +134,8 @@ WebAsyncTaskInfo asyncNavigateForeignKey( @NotNull Integer columnIndex, @NotNull WebSQLResultsRow row, @Nullable String associationName, - @Nullable WebDataFormat dataFormat) throws DBException; + @Nullable WebDataFormat dataFormat, + boolean isReference) throws DBException; /** * Reads dynamic trace from provided database results. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java index c0bf3dfab52..01fef675251 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java @@ -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.service.sql; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataKind; import org.jkiss.dbeaver.model.DBPEvaluationContext; @@ -24,11 +26,12 @@ import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.struct.DBSEntityAssociation; -import org.jkiss.dbeaver.model.struct.DBSEntityReferrer; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; +import org.jkiss.dbeaver.model.struct.*; +import org.jkiss.dbeaver.model.virtual.DBVUtils; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -138,19 +141,60 @@ public DBCLogicalOperator[] getSupportedOperations() { @Property public List getReferences() { + List references = new ArrayList<>(); + + // Forward references: foreign keys where this column is the source List referrers = attrMeta.getReferrers(); - if (referrers == null || referrers.isEmpty()) { - return Collections.emptyList(); + if (referrers != null) { + for (DBSEntityReferrer referrer : referrers) { + if (referrer instanceof DBSEntityAssociation association) { + references.add(new WebSQLQueryResultColumnReference(association, false)); + } + } } - List references = new ArrayList<>(); - for (DBSEntityReferrer referrer : referrers) { - if (referrer instanceof DBSEntityAssociation) { - references.add(new WebSQLQueryResultColumnReference((DBSEntityAssociation) referrer)); + + // Reverse references: foreign keys from other entities targeting this column + DBSEntityAttribute entityAttribute = attrMeta.getEntityAttribute(); + if (entityAttribute != null) { + DBSEntity parentEntity = entityAttribute.getParentObject(); + if (parentEntity != null) { + DBRProgressMonitor monitor = new VoidProgressMonitor(); + for (DBSEntityAssociation reverseRef : DBVUtils.getAllReferences(monitor, parentEntity)) { + try { + if (referenceTargetsAttribute(monitor, reverseRef, entityAttribute)) { + references.add(new WebSQLQueryResultColumnReference(reverseRef, true)); + } + } catch (DBException e) { + log.debug("Error reading attributes for reverse reference " + reverseRef.getName(), e); + } + } } } + return references; } + private static boolean referenceTargetsAttribute( + @NotNull DBRProgressMonitor monitor, + @NotNull DBSEntityAssociation association, + @NotNull DBSEntityAttribute attribute + ) throws DBException { + DBSEntityConstraint refConstraint = association.getReferencedConstraint(); + if (!(refConstraint instanceof DBSEntityReferrer referrer)) { + return false; + } + List attrs = referrer.getAttributeReferences(monitor); + if (attrs == null) { + return false; + } + for (DBSEntityAttributeRef ref : attrs) { + if (attribute.equals(ref.getAttribute())) { + return true; + } + } + return false; + } + @Override public String toString() { return attrMeta.getName(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java index 0a257683273..9bf9b31c94a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java @@ -32,9 +32,11 @@ public class WebSQLQueryResultColumnReference { @NotNull private final DBSEntityAssociation association; + private final boolean reverse; - public WebSQLQueryResultColumnReference(@NotNull DBSEntityAssociation association) { + public WebSQLQueryResultColumnReference(@NotNull DBSEntityAssociation association, boolean reverse) { this.association = association; + this.reverse = reverse; } @NotNull @@ -43,14 +45,28 @@ public String getAssociationName() { return association.getName(); } + /** + * Matches desktop {@code ReferencesResultsContainer.ReferenceKey.isReference}: true for reverse + * references (other entity's FK points to this column), false for forward foreign keys. + */ + @Property + public boolean isReference() { + return reverse; + } + @Nullable @Property public String getTargetEntityName() { - DBSEntityConstraint referencedConstraint = association.getReferencedConstraint(); - if (referencedConstraint == null) { - return null; + DBSEntity targetEntity; + if (reverse) { + targetEntity = association.getParentObject(); + } else { + DBSEntityConstraint referencedConstraint = association.getReferencedConstraint(); + if (referencedConstraint == null) { + return null; + } + targetEntity = referencedConstraint.getParentObject(); } - DBSEntity targetEntity = referencedConstraint.getParentObject(); if (targetEntity == null) { return null; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index 2c9972840d4..2514b8a6f8f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -240,7 +240,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getArgumentVal(env, "columnIndex"), new WebSQLResultsRow(getArgument(env, "row")), getArgument(env, "associationName"), - getDataFormat(env) + getDataFormat(env), + CommonUtils.toBoolean(env.getArgument("isReference")) )) .dataFetcher("asyncSqlExecuteResults", env -> getService(env).asyncGetQueryResults( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index b6409e7efc1..6296807428f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -62,6 +62,7 @@ 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.virtual.DBVUtils; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -611,7 +612,8 @@ public WebAsyncTaskInfo asyncNavigateForeignKey( @NotNull Integer columnIndex, @NotNull WebSQLResultsRow row, @Nullable String associationName, - @Nullable WebDataFormat dataFormat + @Nullable WebDataFormat dataFormat, + boolean isReference ) { WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { @Override @@ -625,40 +627,18 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { throw new DBWebException("Column index '" + columnIndex + "' is out of range"); } DBDAttributeBinding attribute = attributes[columnIndex]; - // Get association - List referrers = attribute.getReferrers(); - if (CommonUtils.isEmpty(referrers)) { - throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); - } - DBSEntityAssociation association = null; - for (DBSEntityReferrer referrer : referrers) { - if (referrer instanceof DBSEntityAssociation) { - DBSEntityAssociation referrerAssociation = (DBSEntityAssociation) referrer; - if (CommonUtils.isEmpty(associationName) || - CommonUtils.equalObjects(associationName, referrerAssociation.getName())) { - association = referrerAssociation; - break; - } - } - } - if (association == null) { - if (CommonUtils.isEmpty(associationName)) { - throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); - } - throw new DBException("Association '" + associationName + "' not found in attribute [" + attribute.getName() + "]"); - } + DBSEntityAssociation association = isReference + ? findReverseAssociation(monitor, attribute, associationName) + : findForwardAssociation(attribute, associationName); WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider( resultsId, contextInfo, List.of(row) ); - DBDReferenceNavigation navigation = DBDReferenceUtils.resolveAssociationNavigation( - monitor, - dataProvider, - association, - dataProvider.getSelectedRows() - ); + 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"); } @@ -682,6 +662,49 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { return webSession.createAndRunAsyncTask("Navigate foreign key from results " + resultsId, runnable); } + @NotNull + private static DBSEntityAssociation findForwardAssociation( + @NotNull DBDAttributeBinding attribute, + @Nullable String associationName + ) throws DBException { + List referrers = attribute.getReferrers(); + if (CommonUtils.isEmpty(referrers)) { + throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); + } + for (DBSEntityReferrer referrer : referrers) { + if (referrer instanceof DBSEntityAssociation referrerAssociation + && (CommonUtils.isEmpty(associationName) + || CommonUtils.equalObjects(associationName, referrerAssociation.getName()))) { + return referrerAssociation; + } + } + if (CommonUtils.isEmpty(associationName)) { + throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); + } + throw new DBException("Association '" + associationName + "' not found in attribute [" + attribute.getName() + "]"); + } + + @NotNull + private static DBSEntityAssociation findReverseAssociation( + @NotNull DBRProgressMonitor monitor, + @NotNull DBDAttributeBinding attribute, + @Nullable String associationName + ) throws DBException { + DBSEntityAttribute entityAttribute = attribute.getEntityAttribute(); + if (entityAttribute == null || entityAttribute.getParentObject() == null) { + throw new DBException("Can't resolve parent entity for attribute [" + attribute.getName() + "]"); + } + if (CommonUtils.isEmpty(associationName)) { + throw new DBException("Reference name is required for reverse navigation on attribute [" + attribute.getName() + "]"); + } + for (DBSEntityAssociation reverseRef : DBVUtils.getAllReferences(monitor, entityAttribute.getParentObject())) { + if (CommonUtils.equalObjects(associationName, reverseRef.getName())) { + return reverseRef; + } + } + throw new DBException("Reverse reference '" + associationName + "' not found for attribute [" + attribute.getName() + "]"); + } + @NotNull @Override public List readDynamicTrace( From b09a60a449e30ae04f0730ac7b6aa1c6877b5d93 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 27 May 2026 15:37:45 +0200 Subject: [PATCH 4/6] dbeaver/pro#8981 api ref panel --- .../schema/service.sql.graphqls | 12 ++-- .../service/sql/WebSQLProcessor.java | 4 +- .../service/sql/WebSQLQueryDataReceiver.java | 2 +- .../service/sql/WebSQLQueryResultColumn.java | 27 ++++---- .../sql/WebSQLQueryResultColumnReference.java | 62 ++++++++++++++----- .../service/sql/WebSQLQueryResultSet.java | 5 +- .../service/sql/impl/WebServiceSQL.java | 6 +- 7 files changed, 79 insertions(+), 39 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 1667965c7a3..6f620d2dd42 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -73,10 +73,14 @@ input SQLDataFilter { } type SQLResultColumnReference { - associationName: String! - targetEntityName: String + "Name of the association (foreign key) that connects this column to the target entity. For reverse references (isReference=true) this is the name of the association in the target entity that points back to this column)" + associationName: String! @since(version: "26.1.0") + "Fully qualified name of the entity the client will navigate to (referenced entity for forward FK, referencing entity for reverse reference)" + targetEntityName: String @since(version: "26.1.0") "True if this is a reverse reference (another entity's FK points to this column); false for forward foreign keys" isReference: Boolean! @since(version: "26.1.0") + "Navigator node path 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 @since(version: "26.1.0") } type SQLResultColumn { @@ -103,8 +107,8 @@ type SQLResultColumn { "Operations supported for this attribute" supportedOperations: [DataTypeLogicalOperation!]! - "Foreign key references for this column, plus reverse references from other tables that point to it (distinguish via SQLResultColumnReference.isReference)" - references: [SQLResultColumnReference!]! + "Foreign key references for this column" + references: [SQLResultColumnReference!]! @since(version: "26.1.0") "Description of the column" description: String @since(version: "25.1.3") diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index df4835bb5d9..d612d74298c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -480,7 +480,7 @@ public WebSQLExecuteInfo updateResultsDataBatch( WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(); updatedResultSet.setResultsInfo(resultsInfo); - updatedResultSet.setColumns(resultsInfo.getAttributes()); + updatedResultSet.setColumns(webSession, resultsInfo.getAttributes()); WebSQLQueryResults updateResults = new WebSQLQueryResults(webSession, dataFormat); updateResults.setUpdateRowCount(totalUpdateCount); @@ -652,7 +652,7 @@ private DBSDataManipulator generateUpdateResultsDataBatch( WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(); updatedResultSet.setResultsInfo(resultsInfo); - updatedResultSet.setColumns(resultsInfo.getAttributes()); + updatedResultSet.setColumns(webSession, resultsInfo.getAttributes()); if (!CommonUtils.isEmpty(updatedRows)) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java index bfb82f93d36..da7138a420f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryDataReceiver.java @@ -170,7 +170,7 @@ public void fetchEnd(@NotNull DBCSession session, @NotNull DBCResultSet resultSe } } - webResultSet.setColumns(bindings); + webResultSet.setColumns(webSession, bindings); webResultSet.setRows(List.of(rows.toArray(new WebSQLQueryResultSetRow[0]))); webResultSet.setHasChildrenCollection(resultSet instanceof DBDSubCollectionResultSet); webResultSet.setSupportsDataFilter(dataContainer.isFeatureSupported(DBSDataContainer.FEATURE_DATA_FILTER)); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java index 01fef675251..1e2e8187b4f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java @@ -16,7 +16,9 @@ */ package io.cloudbeaver.service.sql; +import io.cloudbeaver.model.session.WebSession; 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.DBPDataKind; @@ -41,9 +43,12 @@ public class WebSQLQueryResultColumn { private static final Log log = Log.getLog(WebSQLQueryResultColumn.class); + @Nullable + private final WebSession session; private final DBDAttributeBinding attrMeta; - public WebSQLQueryResultColumn(DBDAttributeBinding attrMeta) { + public WebSQLQueryResultColumn(@Nullable WebSession session, DBDAttributeBinding attrMeta) { + this.session = session; this.attrMeta = attrMeta; } @@ -148,7 +153,7 @@ public List getReferences() { if (referrers != null) { for (DBSEntityReferrer referrer : referrers) { if (referrer instanceof DBSEntityAssociation association) { - references.add(new WebSQLQueryResultColumnReference(association, false)); + references.add(new WebSQLQueryResultColumnReference(session, association, false)); } } } @@ -157,16 +162,14 @@ public List getReferences() { DBSEntityAttribute entityAttribute = attrMeta.getEntityAttribute(); if (entityAttribute != null) { DBSEntity parentEntity = entityAttribute.getParentObject(); - if (parentEntity != null) { - DBRProgressMonitor monitor = new VoidProgressMonitor(); - for (DBSEntityAssociation reverseRef : DBVUtils.getAllReferences(monitor, parentEntity)) { - try { - if (referenceTargetsAttribute(monitor, reverseRef, entityAttribute)) { - references.add(new WebSQLQueryResultColumnReference(reverseRef, true)); - } - } catch (DBException e) { - log.debug("Error reading attributes for reverse reference " + reverseRef.getName(), e); + DBRProgressMonitor monitor = session != null ? session.getProgressMonitor() : new VoidProgressMonitor(); + for (DBSEntityAssociation reverseRef : DBVUtils.getAllReferences(monitor, parentEntity)) { + try { + if (referenceTargetsAttribute(monitor, reverseRef, entityAttribute)) { + references.add(new WebSQLQueryResultColumnReference(session, reverseRef, true)); } + } catch (DBException e) { + log.debug("Error reading attributes for reverse reference " + reverseRef.getName(), e); } } } @@ -174,7 +177,7 @@ public List getReferences() { return references; } - private static boolean referenceTargetsAttribute( + private boolean referenceTargetsAttribute( @NotNull DBRProgressMonitor monitor, @NotNull DBSEntityAssociation association, @NotNull DBSEntityAttribute attribute diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java index 9bf9b31c94a..85240cbcc2e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java @@ -16,11 +16,15 @@ */ package io.cloudbeaver.service.sql; +import io.cloudbeaver.model.session.WebSession; 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; @@ -30,11 +34,20 @@ */ public class WebSQLQueryResultColumnReference { + private static final Log log = Log.getLog(WebSQLQueryResultColumnReference.class); + + @Nullable + private final WebSession session; @NotNull private final DBSEntityAssociation association; private final boolean reverse; - public WebSQLQueryResultColumnReference(@NotNull DBSEntityAssociation association, boolean reverse) { + public WebSQLQueryResultColumnReference( + @Nullable WebSession session, + @NotNull DBSEntityAssociation association, + boolean reverse + ) { + this.session = session; this.association = association; this.reverse = reverse; } @@ -45,10 +58,6 @@ public String getAssociationName() { return association.getName(); } - /** - * Matches desktop {@code ReferencesResultsContainer.ReferenceKey.isReference}: true for reverse - * references (other entity's FK points to this column), false for forward foreign keys. - */ @Property public boolean isReference() { return reverse; @@ -57,19 +66,42 @@ public boolean isReference() { @Nullable @Property public String getTargetEntityName() { - DBSEntity targetEntity; - if (reverse) { - targetEntity = association.getParentObject(); - } else { - DBSEntityConstraint referencedConstraint = association.getReferencedConstraint(); - if (referencedConstraint == null) { - return null; - } - targetEntity = referencedConstraint.getParentObject(); - } + DBSEntity targetEntity = getTargetEntity(); if (targetEntity == null) { return null; } return DBUtils.getObjectFullName(targetEntity, DBPEvaluationContext.UI); } + + @Nullable + @Property + public String getNodePath() { + if (session == null) { + return null; + } + DBSEntity targetEntity = getTargetEntity(); + if (targetEntity == null) { + return null; + } + try { + DBNNode node = session.getNavigatorModelOrThrow() + .getNodeByObject(session.getProgressMonitor(), targetEntity, false); + return node == null ? null : node.getNodeUri(); + } catch (DBException e) { + log.debug("Error resolving navigator node for entity " + targetEntity.getName(), e); + return null; + } + } + + @Nullable + private DBSEntity getTargetEntity() { + if (reverse) { + return association.getParentObject(); + } + DBSEntityConstraint referencedConstraint = association.getReferencedConstraint(); + if (referencedConstraint == null) { + return null; + } + return referencedConstraint.getParentObject(); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java index 3b3291a91b3..ece338a211d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java @@ -16,6 +16,7 @@ */ 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; @@ -67,10 +68,10 @@ public void setColumns(WebSQLQueryResultColumn[] columns) { this.columns = columns; } - public void setColumns(DBDAttributeBinding[] bindings) { + public void setColumns(@Nullable WebSession session, DBDAttributeBinding[] bindings) { WebSQLQueryResultColumn[] columns = new WebSQLQueryResultColumn[bindings.length]; for (int i = 0; i < bindings.length; i++) { - columns[i] = new WebSQLQueryResultColumn(bindings[i]); + columns[i] = new WebSQLQueryResultColumn(session, bindings[i]); } this.columns = columns; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 6296807428f..865f39b64e3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -663,7 +663,7 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { } @NotNull - private static DBSEntityAssociation findForwardAssociation( + private DBSEntityAssociation findForwardAssociation( @NotNull DBDAttributeBinding attribute, @Nullable String associationName ) throws DBException { @@ -685,13 +685,13 @@ private static DBSEntityAssociation findForwardAssociation( } @NotNull - private static DBSEntityAssociation findReverseAssociation( + private DBSEntityAssociation findReverseAssociation( @NotNull DBRProgressMonitor monitor, @NotNull DBDAttributeBinding attribute, @Nullable String associationName ) throws DBException { DBSEntityAttribute entityAttribute = attribute.getEntityAttribute(); - if (entityAttribute == null || entityAttribute.getParentObject() == null) { + if (entityAttribute == null) { throw new DBException("Can't resolve parent entity for attribute [" + attribute.getName() + "]"); } if (CommonUtils.isEmpty(associationName)) { From 1af0d48f3c29e1535e345a343df936dee75cc797 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Fri, 29 May 2026 12:51:02 +0200 Subject: [PATCH 5/6] dbeaver/pro#8981 api ref panel --- .../schema/service.sql.graphqls | 38 ++++--- .../service/sql/DBWServiceSQL.java | 8 +- .../service/sql/WebSQLQueryResultColumn.java | 70 +----------- ...e.java => WebSQLQueryResultReference.java} | 24 ++-- .../service/sql/WebSQLQueryResultSet.java | 105 ++++++++++++++++-- .../service/sql/WebServiceBindingSQL.java | 8 +- .../service/sql/impl/WebServiceSQL.java | 64 +++-------- 7 files changed, 159 insertions(+), 158 deletions(-) rename server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/{WebSQLQueryResultColumnReference.java => WebSQLQueryResultReference.java} (86%) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 6f620d2dd42..b25b03eb2a6 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -72,15 +72,17 @@ input SQLDataFilter { orderBy: String } -type SQLResultColumnReference { - "Name of the association (foreign key) that connects this column to the target entity. For reverse references (isReference=true) this is the name of the association in the target entity that points back to this column)" - associationName: String! @since(version: "26.1.0") - "Fully qualified name of the entity the client will navigate to (referenced entity for forward FK, referencing entity for reverse reference)" - targetEntityName: String @since(version: "26.1.0") - "True if this is a reverse reference (another entity's FK points to this column); false for forward foreign keys" - isReference: Boolean! @since(version: "26.1.0") - "Navigator node path 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 @since(version: "26.1.0") +type SQLResultReference @since(version: "26.1.1") { + "Name of the association (foreign key)" + associationName: String! + "Fully qualified name of the entity" + targetEntityName: String + "True for reverse references (other entities' FKs pointing at the source entity); false for forward foreign keys" + isReference: Boolean! + "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!]! } type SQLResultColumn { @@ -107,9 +109,6 @@ type SQLResultColumn { "Operations supported for this attribute" supportedOperations: [DataTypeLogicalOperation!]! - "Foreign key references for this column" - references: [SQLResultColumnReference!]! @since(version: "26.1.0") - "Description of the column" description: String @since(version: "25.1.3") } @@ -136,6 +135,9 @@ type SQLResultSet { "Returns list of rows in the result set. Each row contains data and metadata" rowsWithMetaData: [SQLResultRowMetaData] @since(version: "23.3.5") + "All FK navigation paths available from this result set, both forward (outgoing) and reverse (incoming). Distinguish via SQLResultReference.isReference" + references: [SQLResultReference!]! @since(version: "26.1.0") + # True means that resultset was generated by single entity query # New rows can be added, old rows can be deleted singleEntity: Boolean! @@ -465,18 +467,18 @@ extend type Mutation { dataFormat: ResultDataFormat ): AsyncTaskInfo! - "Creates async task for reading referenced data by foreign key cell. Set isReference=true to navigate a reverse reference (other entity's FK pointing to this row)." + "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!, - columnIndex: Int!, row: SQLResultRow!, - associationName: String, - dataFormat: ResultDataFormat, - isReference: Boolean @since(version: "26.1.0") - ): AsyncTaskInfo! + 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( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 968dce91023..73bf603143d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -131,11 +131,11 @@ WebAsyncTaskInfo asyncNavigateForeignKey( @NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, - @NotNull Integer columnIndex, @NotNull WebSQLResultsRow row, - @Nullable String associationName, - @Nullable WebDataFormat dataFormat, - boolean isReference) throws DBException; + int columnIndex, + @NotNull String associationName, + boolean isReference, + @Nullable WebDataFormat dataFormat) throws DBException; /** * Reads dynamic trace from provided database results. diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java index 1e2e8187b4f..aa9489960e6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumn.java @@ -16,10 +16,6 @@ */ package io.cloudbeaver.service.sql; -import io.cloudbeaver.model.session.WebSession; -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.DBPDataKind; import org.jkiss.dbeaver.model.DBPEvaluationContext; @@ -28,13 +24,6 @@ import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; -import org.jkiss.dbeaver.model.struct.*; -import org.jkiss.dbeaver.model.virtual.DBVUtils; - -import java.util.ArrayList; -import java.util.List; /** * Web SQL query resultset. @@ -43,12 +32,9 @@ public class WebSQLQueryResultColumn { private static final Log log = Log.getLog(WebSQLQueryResultColumn.class); - @Nullable - private final WebSession session; private final DBDAttributeBinding attrMeta; - public WebSQLQueryResultColumn(@Nullable WebSession session, DBDAttributeBinding attrMeta) { - this.session = session; + public WebSQLQueryResultColumn(DBDAttributeBinding attrMeta) { this.attrMeta = attrMeta; } @@ -144,60 +130,6 @@ public DBCLogicalOperator[] getSupportedOperations() { return attrMeta.getValueHandler().getSupportedOperators(attrMeta); } - @Property - public List getReferences() { - List references = new ArrayList<>(); - - // Forward references: foreign keys where this column is the source - List referrers = attrMeta.getReferrers(); - if (referrers != null) { - for (DBSEntityReferrer referrer : referrers) { - if (referrer instanceof DBSEntityAssociation association) { - references.add(new WebSQLQueryResultColumnReference(session, association, false)); - } - } - } - - // Reverse references: foreign keys from other entities targeting this column - DBSEntityAttribute entityAttribute = attrMeta.getEntityAttribute(); - if (entityAttribute != null) { - DBSEntity parentEntity = entityAttribute.getParentObject(); - DBRProgressMonitor monitor = session != null ? session.getProgressMonitor() : new VoidProgressMonitor(); - for (DBSEntityAssociation reverseRef : DBVUtils.getAllReferences(monitor, parentEntity)) { - try { - if (referenceTargetsAttribute(monitor, reverseRef, entityAttribute)) { - references.add(new WebSQLQueryResultColumnReference(session, reverseRef, true)); - } - } catch (DBException e) { - log.debug("Error reading attributes for reverse reference " + reverseRef.getName(), e); - } - } - } - - return references; - } - - private boolean referenceTargetsAttribute( - @NotNull DBRProgressMonitor monitor, - @NotNull DBSEntityAssociation association, - @NotNull DBSEntityAttribute attribute - ) throws DBException { - DBSEntityConstraint refConstraint = association.getReferencedConstraint(); - if (!(refConstraint instanceof DBSEntityReferrer referrer)) { - return false; - } - List attrs = referrer.getAttributeReferences(monitor); - if (attrs == null) { - return false; - } - for (DBSEntityAttributeRef ref : attrs) { - if (attribute.equals(ref.getAttribute())) { - return true; - } - } - return false; - } - @Override public String toString() { return attrMeta.getName(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultReference.java similarity index 86% rename from server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java rename to server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultReference.java index 85240cbcc2e..469b23c8c5b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultColumnReference.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultReference.java @@ -29,27 +29,30 @@ import org.jkiss.dbeaver.model.struct.DBSEntityAssociation; import org.jkiss.dbeaver.model.struct.DBSEntityConstraint; -/** - * Web SQL query result column reference. - */ -public class WebSQLQueryResultColumnReference { +import java.util.List; + +public class WebSQLQueryResultReference { - private static final Log log = Log.getLog(WebSQLQueryResultColumnReference.class); + private static final Log log = Log.getLog(WebSQLQueryResultReference.class); @Nullable private final WebSession session; @NotNull private final DBSEntityAssociation association; private final boolean reverse; + @NotNull + private final List columnIndexList; - public WebSQLQueryResultColumnReference( + public WebSQLQueryResultReference( @Nullable WebSession session, @NotNull DBSEntityAssociation association, - boolean reverse + boolean reverse, + @NotNull List columnIndexList ) { this.session = session; this.association = association; this.reverse = reverse; + this.columnIndexList = columnIndexList; } @NotNull @@ -73,6 +76,7 @@ public String getTargetEntityName() { return DBUtils.getObjectFullName(targetEntity, DBPEvaluationContext.UI); } + @Nullable @Property public String getNodePath() { @@ -93,6 +97,12 @@ public String getNodePath() { } } + @NotNull + @Property + public List getColumnIndexList() { + return columnIndexList; + } + @Nullable private DBSEntity getTargetEntity() { if (reverse) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java index ece338a211d..088724ca563 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java @@ -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. @@ -20,14 +20,17 @@ import io.cloudbeaver.service.sql.WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState; 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.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.exec.DBCExecutionContext; import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.struct.*; -import java.util.Collections; -import java.util.List; +import java.util.*; +import java.util.function.Function; /** * Web SQL query resultset. @@ -37,6 +40,7 @@ public class WebSQLQueryResultSet { private static final Log log = Log.getLog(WebSQLQueryResultSet.class); private WebSQLQueryResultColumn[] columns; + private List references = Collections.emptyList(); private List rows = Collections.emptyList(); private boolean hasMoreData; private WebSQLResultsInfo resultsInfo; @@ -68,12 +72,19 @@ public void setColumns(WebSQLQueryResultColumn[] columns) { this.columns = columns; } - public void setColumns(@Nullable WebSession session, DBDAttributeBinding[] bindings) { - WebSQLQueryResultColumn[] columns = new WebSQLQueryResultColumn[bindings.length]; + public void setColumns(@NotNull WebSession session, @NotNull DBDAttributeBinding[] bindings) { + WebSQLQueryResultColumn[] cols = new WebSQLQueryResultColumn[bindings.length]; for (int i = 0; i < bindings.length; i++) { - columns[i] = new WebSQLQueryResultColumn(session, bindings[i]); + cols[i] = new WebSQLQueryResultColumn(bindings[i]); } - this.columns = columns; + this.columns = cols; + this.references = collectReferences(session, bindings); + } + + @NotNull + @Property + public List getReferences() { + return references; } @Property @@ -190,4 +201,84 @@ public void setReadOnlyInfo(@Nullable DBCExecutionContext executionContext) { this.readOnlyStatus = DBExecUtils.getResultSetReadOnlyStatus( executionContext == null ? null : executionContext.getDataSource().getContainer()); } + + @NotNull + public static List collectReferences( + @NotNull WebSession session, + @NotNull DBDAttributeBinding[] bindings + ) { + Map attrToIndex = new HashMap<>(); + LinkedHashSet entities = new LinkedHashSet<>(); + for (int i = 0; i < bindings.length; i++) { + DBSEntityAttribute ea = bindings[i].getEntityAttribute(); + if (ea == null) { + continue; + } + attrToIndex.putIfAbsent(ea, i); + DBSEntity parent = ea.getParentObject(); + entities.add(parent); + } + + Function attrToBinding = attr -> { + Integer idx = attrToIndex.get(attr); + return idx == null ? null : bindings[idx]; + }; + + List result = new ArrayList<>(); + DBRProgressMonitor monitor = session.getProgressMonitor(); + for (DBSEntity entity : entities) { + try { + for (DBSEntityAssociation fk : DBExecUtils.readAssociations(monitor, entity, attrToBinding)) { + List columnIndex = collectOwnColumnIndex(monitor, fk, false, attrToIndex); + if (columnIndex != null) { + result.add(new WebSQLQueryResultReference(session, fk, false, columnIndex)); + } + } + for (DBSEntityAssociation ref : DBExecUtils.readReferences(monitor, entity, attrToBinding)) { + List columnIndex = collectOwnColumnIndex(monitor, ref, true, attrToIndex); + if (columnIndex != null) { + result.add(new WebSQLQueryResultReference(session, ref, true, columnIndex)); + } + } + } catch (DBException e) { + log.debug("Error collecting references for entity " + entity.getName(), e); + } + } + return result; + } + + @Nullable + private static List collectOwnColumnIndex( + @NotNull DBRProgressMonitor monitor, + @NotNull DBSEntityAssociation association, + boolean reverse, + @NotNull Map 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 attrs = ownSide.getAttributeReferences(monitor); + if (attrs == null || attrs.isEmpty()) { + return null; + } + List indexList = new ArrayList<>(attrs.size()); + for (DBSEntityAttributeRef attrRef : attrs) { + Integer idx = attrToIndex.get(attrRef.getAttribute()); + if (idx == null) { + return null; + } + indexList.add(idx); + } + return indexList; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index 2514b8a6f8f..abc3ed90798 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -237,11 +237,11 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getWebSession(env), getSQLContext(env), getArgumentVal(env, "resultsId"), - getArgumentVal(env, "columnIndex"), new WebSQLResultsRow(getArgument(env, "row")), - getArgument(env, "associationName"), - getDataFormat(env), - CommonUtils.toBoolean(env.getArgument("isReference")) + getArgumentVal(env, "columnIndex"), + getArgumentVal(env, "associationName"), + CommonUtils.toBoolean(env.getArgument("isReference")), + getDataFormat(env) )) .dataFetcher("asyncSqlExecuteResults", env -> getService(env).asyncGetQueryResults( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 865f39b64e3..ff2c98b4f24 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -62,7 +62,6 @@ 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.virtual.DBVUtils; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -609,27 +608,22 @@ public WebAsyncTaskInfo asyncNavigateForeignKey( @NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, - @NotNull Integer columnIndex, @NotNull WebSQLResultsRow row, - @Nullable String associationName, - @Nullable WebDataFormat dataFormat, - boolean isReference + int columnIndex, + @NotNull String associationName, + boolean isReference, + @Nullable WebDataFormat dataFormat ) { WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { @Override public void run(DBRProgressMonitor monitor) throws InvocationTargetException { try { monitor.beginTask("Navigate foreign key", 1); - //Get bindings WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); - DBDAttributeBinding[] attributes = resultsInfo.getAttributes(); - if (columnIndex < 0 || columnIndex >= attributes.length) { - throw new DBWebException("Column index '" + columnIndex + "' is out of range"); - } - DBDAttributeBinding attribute = attributes[columnIndex]; + DBSEntity sourceEntity = resolveSourceEntity(resultsInfo.getAttributes(), columnIndex); DBSEntityAssociation association = isReference - ? findReverseAssociation(monitor, attribute, associationName) - : findForwardAssociation(attribute, associationName); + ? DBExecUtils.findReverseAssociationByName(monitor, sourceEntity, associationName) + : DBExecUtils.findForwardAssociationByName(monitor, sourceEntity, associationName); WebDBDResultSetDataProvider dataProvider = new WebDBDResultSetDataProvider( resultsId, @@ -663,46 +657,18 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { } @NotNull - private DBSEntityAssociation findForwardAssociation( - @NotNull DBDAttributeBinding attribute, - @Nullable String associationName + private static DBSEntity resolveSourceEntity( + @NotNull DBDAttributeBinding[] attributes, + int columnIndex ) throws DBException { - List referrers = attribute.getReferrers(); - if (CommonUtils.isEmpty(referrers)) { - throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); + if (columnIndex < 0 || columnIndex >= attributes.length) { + throw new DBWebException("Column index '" + columnIndex + "' is out of range"); } - for (DBSEntityReferrer referrer : referrers) { - if (referrer instanceof DBSEntityAssociation referrerAssociation - && (CommonUtils.isEmpty(associationName) - || CommonUtils.equalObjects(associationName, referrerAssociation.getName()))) { - return referrerAssociation; - } - } - if (CommonUtils.isEmpty(associationName)) { - throw new DBException("Association not found in attribute [" + attribute.getName() + "]"); - } - throw new DBException("Association '" + associationName + "' not found in attribute [" + attribute.getName() + "]"); - } - - @NotNull - private DBSEntityAssociation findReverseAssociation( - @NotNull DBRProgressMonitor monitor, - @NotNull DBDAttributeBinding attribute, - @Nullable String associationName - ) throws DBException { - DBSEntityAttribute entityAttribute = attribute.getEntityAttribute(); + DBSEntityAttribute entityAttribute = attributes[columnIndex].getEntityAttribute(); if (entityAttribute == null) { - throw new DBException("Can't resolve parent entity for attribute [" + attribute.getName() + "]"); - } - if (CommonUtils.isEmpty(associationName)) { - throw new DBException("Reference name is required for reverse navigation on attribute [" + attribute.getName() + "]"); - } - for (DBSEntityAssociation reverseRef : DBVUtils.getAllReferences(monitor, entityAttribute.getParentObject())) { - if (CommonUtils.equalObjects(associationName, reverseRef.getName())) { - return reverseRef; - } + throw new DBException("Column [" + attributes[columnIndex].getName() + "] is not bound to any entity"); } - throw new DBException("Reverse reference '" + associationName + "' not found for attribute [" + attribute.getName() + "]"); + return entityAttribute.getParentObject(); } @NotNull From 839b053eb86e2320d585dfd22f643eff4c7e4b5c Mon Sep 17 00:00:00 2001 From: Ruslan Date: Fri, 29 May 2026 16:44:25 +0200 Subject: [PATCH 6/6] dbeaver/pro#8981 add api ref to table level --- .../service/sql/WebSQLQueryResultSet.java | 95 ++----------------- .../cloudbeaver/service/sql/WebSQLUtils.java | 88 ++++++++++++++++- .../service/sql/impl/WebServiceSQL.java | 2 +- 3 files changed, 92 insertions(+), 93 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java index 088724ca563..a7ce4459277 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLQueryResultSet.java @@ -20,17 +20,14 @@ import io.cloudbeaver.service.sql.WebSQLResultSetRowIdentifier.WebSQLResultSetRowIdentifierState; 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.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.exec.DBCExecutionContext; import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.meta.Property; -import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.struct.*; -import java.util.*; -import java.util.function.Function; +import java.util.Collections; +import java.util.List; /** * Web SQL query resultset. @@ -73,12 +70,12 @@ public void setColumns(WebSQLQueryResultColumn[] columns) { } public void setColumns(@NotNull WebSession session, @NotNull DBDAttributeBinding[] bindings) { - WebSQLQueryResultColumn[] cols = new WebSQLQueryResultColumn[bindings.length]; + WebSQLQueryResultColumn[] columns = new WebSQLQueryResultColumn[bindings.length]; for (int i = 0; i < bindings.length; i++) { - cols[i] = new WebSQLQueryResultColumn(bindings[i]); + columns[i] = new WebSQLQueryResultColumn(bindings[i]); } - this.columns = cols; - this.references = collectReferences(session, bindings); + this.columns = columns; + this.references = WebSQLUtils.collectReferences(session, bindings); } @NotNull @@ -201,84 +198,4 @@ public void setReadOnlyInfo(@Nullable DBCExecutionContext executionContext) { this.readOnlyStatus = DBExecUtils.getResultSetReadOnlyStatus( executionContext == null ? null : executionContext.getDataSource().getContainer()); } - - @NotNull - public static List collectReferences( - @NotNull WebSession session, - @NotNull DBDAttributeBinding[] bindings - ) { - Map attrToIndex = new HashMap<>(); - LinkedHashSet entities = new LinkedHashSet<>(); - for (int i = 0; i < bindings.length; i++) { - DBSEntityAttribute ea = bindings[i].getEntityAttribute(); - if (ea == null) { - continue; - } - attrToIndex.putIfAbsent(ea, i); - DBSEntity parent = ea.getParentObject(); - entities.add(parent); - } - - Function attrToBinding = attr -> { - Integer idx = attrToIndex.get(attr); - return idx == null ? null : bindings[idx]; - }; - - List result = new ArrayList<>(); - DBRProgressMonitor monitor = session.getProgressMonitor(); - for (DBSEntity entity : entities) { - try { - for (DBSEntityAssociation fk : DBExecUtils.readAssociations(monitor, entity, attrToBinding)) { - List columnIndex = collectOwnColumnIndex(monitor, fk, false, attrToIndex); - if (columnIndex != null) { - result.add(new WebSQLQueryResultReference(session, fk, false, columnIndex)); - } - } - for (DBSEntityAssociation ref : DBExecUtils.readReferences(monitor, entity, attrToBinding)) { - List columnIndex = collectOwnColumnIndex(monitor, ref, true, attrToIndex); - if (columnIndex != null) { - result.add(new WebSQLQueryResultReference(session, ref, true, columnIndex)); - } - } - } catch (DBException e) { - log.debug("Error collecting references for entity " + entity.getName(), e); - } - } - return result; - } - - @Nullable - private static List collectOwnColumnIndex( - @NotNull DBRProgressMonitor monitor, - @NotNull DBSEntityAssociation association, - boolean reverse, - @NotNull Map 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 attrs = ownSide.getAttributeReferences(monitor); - if (attrs == null || attrs.isEmpty()) { - return null; - } - List indexList = new ArrayList<>(attrs.size()); - for (DBSEntityAttributeRef attrRef : attrs) { - Integer idx = attrToIndex.get(attrRef.getAttribute()); - if (idx == null) { - return null; - } - indexList.add(idx); - } - return indexList; - } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java index 32ddbfcdb08..6bf4437892f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java @@ -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. @@ -26,17 +26,18 @@ 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.DBPEvaluationContext; import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.exec.DBCSession; +import org.jkiss.dbeaver.model.exec.DBExecUtils; import org.jkiss.dbeaver.model.gis.DBGeometry; import org.jkiss.dbeaver.model.gis.GisConstants; import org.jkiss.dbeaver.model.gis.GisTransformUtils; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.model.struct.DBSAttributeBase; -import org.jkiss.dbeaver.model.struct.DBSTypedObject; +import org.jkiss.dbeaver.model.struct.*; import org.jkiss.dbeaver.model.websocket.event.WSEvent; import org.jkiss.dbeaver.utils.ContentUtils; import org.jkiss.dbeaver.utils.GeneralUtils; @@ -50,6 +51,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Function; /** * Web SQL utils. @@ -329,4 +331,84 @@ public static T requestConfirmation( webSession.removeAttribute(attributeName); } } + + @NotNull + public static List collectReferences( + @NotNull WebSession session, + @NotNull DBDAttributeBinding[] bindings + ) { + Map attrToIndex = new HashMap<>(); + LinkedHashSet entities = new LinkedHashSet<>(); + for (int i = 0; i < bindings.length; i++) { + DBSEntityAttribute ea = bindings[i].getEntityAttribute(); + if (ea == null) { + continue; + } + attrToIndex.putIfAbsent(ea, i); + DBSEntity parent = ea.getParentObject(); + entities.add(parent); + } + + Function attrToBinding = attr -> { + Integer idx = attrToIndex.get(attr); + return idx == null ? null : bindings[idx]; + }; + + List result = new ArrayList<>(); + DBRProgressMonitor monitor = session.getProgressMonitor(); + for (DBSEntity entity : entities) { + try { + for (DBSEntityAssociation fk : DBExecUtils.readAssociations(monitor, entity, attrToBinding)) { + List columnIndex = collectOwnColumnIndex(monitor, fk, false, attrToIndex); + if (columnIndex != null) { + result.add(new WebSQLQueryResultReference(session, fk, false, columnIndex)); + } + } + for (DBSEntityAssociation ref : DBExecUtils.readReferences(monitor, entity, attrToBinding)) { + List columnIndex = collectOwnColumnIndex(monitor, ref, true, attrToIndex); + if (columnIndex != null) { + result.add(new WebSQLQueryResultReference(session, ref, true, columnIndex)); + } + } + } catch (DBException e) { + log.debug("Error collecting references for entity " + entity.getName(), e); + } + } + return result; + } + + @Nullable + private static List collectOwnColumnIndex( + @NotNull DBRProgressMonitor monitor, + @NotNull DBSEntityAssociation association, + boolean reverse, + @NotNull Map 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 attrs = ownSide.getAttributeReferences(monitor); + if (attrs == null || attrs.isEmpty()) { + return null; + } + List indexList = new ArrayList<>(attrs.size()); + for (DBSEntityAttributeRef attrRef : attrs) { + Integer idx = attrToIndex.get(attrRef.getAttribute()); + if (idx == null) { + return null; + } + indexList.add(idx); + } + return indexList; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index ff2c98b4f24..38809ff548e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -657,7 +657,7 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException { } @NotNull - private static DBSEntity resolveSourceEntity( + private DBSEntity resolveSourceEntity( @NotNull DBDAttributeBinding[] attributes, int columnIndex ) throws DBException {