diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index 3155e9175e..0b70ecb348 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,18 @@ 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, + 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 42b552084d..d4ec200156 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,16 @@ 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 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 a3a5778b3c..c0bf3dfab5 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 0000000000..0a25768327 --- /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 bde7777ba2..2c9972840d 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,16 @@ 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"), + 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 5ff65671c6..b6409e7efc 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,85 @@ 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 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, + null, + WebSQLDataFilter.from(navigation.getTargetFilter()), + dataFormat + ); + this.result = executeResults.getStatusMessage(); + this.extendedResults = executeResults; + } catch (Throwable e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + }; + return webSession.createAndRunAsyncTask("Navigate foreign key from results " + resultsId, runnable); + } + @NotNull @Override public List readDynamicTrace(