Skip to content
29 changes: 29 additions & 0 deletions server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ input SQLDataFilter {
orderBy: String
}

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 {
position: Int!
name: String
Expand Down Expand Up @@ -122,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!
Expand Down Expand Up @@ -451,6 +467,19 @@ extend type Mutation {
dataFormat: ResultDataFormat
): AsyncTaskInfo!

"Creates async task for navigating a foreign-key reference exposed in SQLResultSet.references. Pass associationName, isReference and any one of the chosen reference's columnIndices (used to locate the source entity)."
asyncSqlNavigateForeignKey(
projectId: ID,
connectionId: ID!,
contextId: ID!,
resultsId: ID!,
row: SQLResultRow!,
columnIndex: Int!,
associationName: String!,
isReference: Boolean!,
dataFormat: ResultDataFormat
): AsyncTaskInfo! @since(version: "26.1.1")

"Returns transaction log info for the specified project, connection and context"
getTransactionLogInfo(
projectId: ID!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@
@Nullable WebSQLDataFilter filter,
@Nullable WebDataFormat dataFormat) throws DBWebException;

@WebAction
WebAsyncTaskInfo asyncNavigateForeignKey(

Check warning on line 130 in server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java

View workflow job for this annotation

GitHub Actions / Server / Lint

[checkstyle] reported by reviewdog 🐶 Reference type 'WebAsyncTaskInfo' is missing a nullability annotation. Raw Output: /github/workspace/./server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java:130:5: warning: Reference type 'WebAsyncTaskInfo' is missing a nullability annotation. (sh.adelessfox.checkstyle.checks.NullabilityAnnotationsCheck)
@NotNull WebSession webSession,
@NotNull WebSQLContextInfo contextInfo,
@NotNull String resultsId,
@NotNull WebSQLResultsRow row,
int columnIndex,
@NotNull String associationName,
boolean isReference,
@Nullable WebDataFormat dataFormat) throws DBException;

/**
* Reads dynamic trace from provided database results.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 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;

import java.util.List;

public class WebSQLQueryResultReference {

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<Integer> columnIndexList;

public WebSQLQueryResultReference(
@Nullable WebSession session,
@NotNull DBSEntityAssociation association,
boolean reverse,
@NotNull List<Integer> columnIndexList
) {
this.session = session;
this.association = association;
this.reverse = reverse;
this.columnIndexList = columnIndexList;
}

@NotNull
@Property
public String getAssociationName() {
return association.getName();
}

@Property
public boolean isReference() {
return reverse;
}

@Nullable
@Property
public String getTargetEntityName() {
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;
}
}

@NotNull
@Property
public List<Integer> getColumnIndexList() {
return columnIndexList;
}

@Nullable
private DBSEntity getTargetEntity() {
if (reverse) {
return association.getParentObject();
}
DBSEntityConstraint referencedConstraint = association.getReferencedConstraint();
if (referencedConstraint == null) {
return null;
}
return referencedConstraint.getParentObject();
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -36,6 +37,7 @@ public class WebSQLQueryResultSet {
private static final Log log = Log.getLog(WebSQLQueryResultSet.class);

private WebSQLQueryResultColumn[] columns;
private List<WebSQLQueryResultReference> references = Collections.emptyList();
private List<WebSQLQueryResultSetRow> rows = Collections.emptyList();
private boolean hasMoreData;
private WebSQLResultsInfo resultsInfo;
Expand Down Expand Up @@ -67,12 +69,19 @@ public void setColumns(WebSQLQueryResultColumn[] columns) {
this.columns = columns;
}

public void setColumns(DBDAttributeBinding[] bindings) {
public void setColumns(@NotNull WebSession session, @NotNull DBDAttributeBinding[] bindings) {
WebSQLQueryResultColumn[] columns = new WebSQLQueryResultColumn[bindings.length];
for (int i = 0; i < bindings.length; i++) {
columns[i] = new WebSQLQueryResultColumn(bindings[i]);
}
this.columns = columns;
this.references = WebSQLUtils.collectReferences(session, bindings);
}

@NotNull
@Property
public List<WebSQLQueryResultReference> getReferences() {
return references;
}

@Property
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -329,4 +331,84 @@ public static <T> T requestConfirmation(
webSession.removeAttribute(attributeName);
}
}

@NotNull
public static List<WebSQLQueryResultReference> collectReferences(
@NotNull WebSession session,
@NotNull DBDAttributeBinding[] bindings
) {
Map<DBSEntityAttribute, Integer> attrToIndex = new HashMap<>();
LinkedHashSet<DBSEntity> 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<DBSEntityAttribute, DBDAttributeBinding> attrToBinding = attr -> {
Integer idx = attrToIndex.get(attr);
return idx == null ? null : bindings[idx];
};

List<WebSQLQueryResultReference> result = new ArrayList<>();
DBRProgressMonitor monitor = session.getProgressMonitor();
for (DBSEntity entity : entities) {
try {
for (DBSEntityAssociation fk : DBExecUtils.readAssociations(monitor, entity, attrToBinding)) {
List<Integer> 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<Integer> 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<Integer> collectOwnColumnIndex(
@NotNull DBRProgressMonitor monitor,
@NotNull DBSEntityAssociation association,
boolean reverse,
@NotNull Map<DBSEntityAttribute, Integer> attrToIndex
) throws DBException {
DBSEntityReferrer ownSide;
if (reverse) {
DBSEntityConstraint refConstraint = association.getReferencedConstraint();
if (!(refConstraint instanceof DBSEntityReferrer referrer)) {
return null;
}
ownSide = referrer;
} else {
if (!(association instanceof DBSEntityReferrer associationRef)) {
return null;
}
ownSide = associationRef;
}
List<? extends DBSEntityAttributeRef> attrs = ownSide.getAttributeReferences(monitor);
if (attrs == null || attrs.isEmpty()) {
return null;
}
List<Integer> 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;
}
}
Loading
Loading