Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
22f213b
wip: top level update translation
a-alle Oct 13, 2025
d1b58b4
Merge branch 'migrate-update-new-translation-layer' into update-trans…
angrykoala Oct 14, 2025
5448ee4
wip: top level update
a-alle Oct 15, 2025
75741de
wip: nested create
a-alle Oct 15, 2025
d48ef35
wip: nested comment
a-alle Oct 15, 2025
092a626
wip: nested delete
a-alle Oct 16, 2025
b75005d
wip: nested disconnect
a-alle Oct 16, 2025
7497851
wip: nested update in progress
a-alle Oct 16, 2025
f1ae097
Nested update
angrykoala Oct 16, 2025
25a4b68
Add style to the QueryAST print tree
angrykoala Oct 16, 2025
96317c6
Fix interfaces for nested update
angrykoala Oct 20, 2025
b936c77
Fix top level pattern in update
angrykoala Oct 20, 2025
3a99fdc
disconnect on interface relationships
a-alle Oct 20, 2025
2b92b20
update on declarerelationship relationships
a-alle Oct 20, 2025
7f9531c
populatedBy and empty match fix
angrykoala Oct 20, 2025
0630d89
Fix filters and set on edges
angrykoala Oct 21, 2025
97e5774
add push input field
a-alle Oct 21, 2025
1eb8041
add pop input field
a-alle Oct 21, 2025
6181c3b
Add fix for aggregation subqueries
angrykoala Oct 22, 2025
4de9098
Add cypher.minus to popInput
angrykoala Oct 22, 2025
dbdcfd1
Fix delete on unions in update
angrykoala Oct 23, 2025
35f677d
WIP math operations
angrykoala Oct 27, 2025
4979c75
WIP math operations
angrykoala Oct 27, 2025
6b1e149
Fix conflicting properties
angrykoala Oct 28, 2025
55db1d7
Fix some ambiguous updates
angrykoala Oct 28, 2025
800e751
Fix set check of null values on required fields
angrykoala Oct 29, 2025
d29861d
Fix comments from review
angrykoala Oct 29, 2025
80b10e3
Merge pull request #6774 from neo4j/fix-tests-update
angrykoala Oct 29, 2025
2864849
Fix populatedBy on new translation layer for update
angrykoala Oct 29, 2025
e429f4d
Merge remote-tracking branch 'origin/update-translation-layer' into u…
angrykoala Oct 29, 2025
90265de
Code review fixes
angrykoala Oct 29, 2025
585f54a
Merge pull request #6775 from neo4j/update-populated-by-new-translati…
angrykoala Oct 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"@graphql-tools/resolvers-composition": "^7.0.0",
"@graphql-tools/schema": "^10.0.0",
"@graphql-tools/utils": "10.9.1",
"@neo4j/cypher-builder": "2.8.1",
"@neo4j/cypher-builder": "2.10.0",
"camelcase": "^6.3.0",
"debug": "^4.3.4",
"dot-prop": "^6.0.1",
Expand Down
11 changes: 6 additions & 5 deletions packages/graphql/src/classes/GraphElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
*/

import type {
CypherField,
PrimitiveField,
BaseField,
CustomEnumField,
CustomResolverField,
CustomScalarField,
TemporalField,
CypherField,
PointField,
CustomResolverField,
BaseField,
PrimitiveField,
TemporalField,
} from "../types";

export interface GraphElementConstructor {
Expand All @@ -40,6 +40,7 @@ export interface GraphElementConstructor {
customResolverFields: CustomResolverField[];
}

/** @deprecated */
export abstract class GraphElement {
public name: string;
public description?: string;
Expand Down
1 change: 1 addition & 0 deletions packages/graphql/src/classes/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export type SubscriptionEvents = {
delete_relationship: string;
};

/** @deprecated */
class Node extends GraphElement {
public relationFields: RelationField[];
public connectionFields: ConnectionField[];
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/classes/Relationship.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface RelationshipConstructor {
pointFields?: PointField[];
customResolverFields?: CustomResolverField[];
}

/** @deprecated */
class Relationship extends GraphElement {
public properties?: string;
public source: string;
Expand Down
7 changes: 5 additions & 2 deletions packages/graphql/src/schema/resolvers/mutation/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphq
import { execute } from "../../../utils";
import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree";
import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation";
import { translateUpdate2 } from "../../../translate/translate-update";

export function updateResolver({
node,
Expand All @@ -42,7 +43,8 @@ export function updateResolver({

(context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree;

const [cypher, params] = await translateUpdate({ context: context as Neo4jGraphQLTranslationContext, node });
// const [cypher, params] = await translateUpdate({ context: context as Neo4jGraphQLTranslationContext, node });
const { cypher, params } = await translateUpdate2({ context: context as Neo4jGraphQLTranslationContext, node });
const executeResult = await execute({
cypher,
params,
Expand All @@ -64,7 +66,8 @@ export function updateResolver({

if (nodeProjection) {
const nodeKey = nodeProjection.alias ? nodeProjection.alias.value : nodeProjection.name.value;
resolveResult[nodeKey] = executeResult.records[0]?.data || [];
// resolveResult[nodeKey] = executeResult.records[0]?.data || [];
resolveResult[nodeKey] = executeResult.records.map((x) => x.this);
}

return resolveResult;
Expand Down
22 changes: 20 additions & 2 deletions packages/graphql/src/translate/queryAST/ast/QueryAST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ConnectionReadOperation } from "./operations/ConnectionReadOperation";
import { DeleteOperation } from "./operations/DeleteOperation";
import { ReadOperation } from "./operations/ReadOperation";
import { TopLevelCreateMutationOperation } from "./operations/TopLevelCreateMutationOperation";
import { TopLevelUpdateMutationOperation } from "./operations/TopLevelUpdateMutationOperation";
import { UnwindCreateOperation } from "./operations/UnwindCreateOperation";
import type { Operation, OperationTranspileResult } from "./operations/operations";

Expand Down Expand Up @@ -87,7 +88,8 @@ export class QueryAST {
this.operation instanceof DeleteOperation ||
this.operation instanceof AggregationOperation ||
this.operation instanceof UnwindCreateOperation ||
this.operation instanceof TopLevelCreateMutationOperation
this.operation instanceof TopLevelCreateMutationOperation ||
this.operation instanceof TopLevelUpdateMutationOperation
) {
return createNode(varName);
}
Expand All @@ -105,7 +107,7 @@ function getTreeLines(treeNode: QueryASTNode, depth: number = 0): string[] {

const line = "────";
if (depth === 0) {
resultLines.push(`${nodeName}`);
resultLines.push(getTopLevelNodeName(nodeName));
} else if (depth === 1) {
resultLines.push(`|${line} ${nodeName}`);
} else {
Expand All @@ -129,3 +131,19 @@ function getTreeLines(treeNode: QueryASTNode, depth: number = 0): string[] {

return resultLines;
}

function getTopLevelNodeName(nodeName: string): string {
const currentMonth = new Date().getMonth();
const isApril = currentMonth === 3;
const isOctober = currentMonth === 9;
const isDecember = currentMonth === 11;
if (isOctober) {
return `${nodeName} \u{1F383}\u{1F383}\u{1F383}`;
} else if (isDecember) {
return `${nodeName} \u{1F384}\u{1F384}\u{1F384}`;
} else if (isApril) {
return `${nodeName} \u{1F430}\u{1F430}\u{1F430}`;
} else {
return `${nodeName}`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ export class IdField extends InputField {
return [];
}

public print(): string {
return `${super.print()} <${this.name}>`;
}

public getSetParams(queryASTContext: QueryASTContext<Cypher.Node>): Cypher.SetParam[] {
const target = this.getTarget(queryASTContext);
const setParam: Cypher.SetParam = [target.property(this.attribute.databaseName), Cypher.randomUUID()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export abstract class InputField extends QueryASTNode {
return [];
}

public getPredicate(_queryASTContext: QueryASTContext): Cypher.Predicate | undefined {
return undefined;
}
protected getTarget(queryASTContext: QueryASTContext<Cypher.Node>): Cypher.Node | Cypher.Relationship {
const target = this.attachedTo === "node" ? queryASTContext.target : queryASTContext.relationship;
if (!target) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export class ParamInputField extends InputField {
protected attribute: AttributeAdapter;
protected inputValue: unknown;

private memoizedParam: Cypher.Param | undefined;

constructor({
attribute,
attachedTo,
Expand All @@ -59,23 +61,43 @@ export class ParamInputField extends InputField {
queryASTContext: QueryASTContext<Cypher.Node>,
_inputVariable?: Cypher.Variable
): Cypher.SetParam[] {
const target = this.getTarget(queryASTContext);
// This check is needed for populatedBy callbacks
const param = this.getParam();
if (param instanceof Cypher.Param) {
if (param.value === undefined) {
return [];
}
}
const leftExpr = this.getLeftExpression(queryASTContext);
const rightExpr = this.getRightExpression(queryASTContext);

const setField: Cypher.SetParam = [leftExpr, rightExpr];
return [setField];
}

protected getLeftExpression(queryASTContext: QueryASTContext<Cypher.Node>): Cypher.Property {
return this.getTarget(queryASTContext).property(this.attribute.databaseName);
}

let rightVariable: Cypher.Expr;
protected getParam(): Cypher.Variable {
if (this.inputValue instanceof Cypher.Variable) {
rightVariable = this.inputValue;
return this.inputValue;
} else {
rightVariable = new Cypher.Param(this.inputValue);
if (!this.memoizedParam) {
this.memoizedParam = new Cypher.Param(this.inputValue);
}
return this.memoizedParam;
}
}

const leftExpr = target.property(this.attribute.databaseName);
const rightExpr = this.coerceReference(rightVariable);

const setField: Cypher.SetParam = [leftExpr, rightExpr];
return [setField];
protected getRightExpression(
_: QueryASTContext<Cypher.Node>
): Exclude<Cypher.Expr, Cypher.Map | Cypher.MapProjection> {
const rightVariable = this.getParam();
return this.coerceReference(rightVariable);
}

private coerceReference(
protected coerceReference(
variable: Cypher.Variable | Cypher.Property
): Exclude<Cypher.Expr, Cypher.Map | Cypher.MapProjection> {
if (this.attribute.typeHelper.isSpatial()) {
Expand Down Expand Up @@ -104,7 +126,6 @@ export class ParamInputField extends InputField {
const mapTime = Cypher.time(comprehensionVar);
return new Cypher.ListComprehension(comprehensionVar, variable).map(mapTime);
}

return variable;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/

import Cypher from "@neo4j/cypher-builder";
import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter";
import { type QueryASTContext } from "../../QueryASTContext";
import { ParamInputField } from "../ParamInputField";

type MathOperator = "increment" | "decrement" | "add" | "subtract" | "divide" | "multiply";

export class MathInputField extends ParamInputField {
private operation: MathOperator;

constructor({
attribute,
attachedTo,
inputValue,
operation,
}: {
attribute: AttributeAdapter;
attachedTo: "node" | "relationship";
inputValue: unknown;
operation: MathOperator;
}) {
super({ attribute, attachedTo, inputValue });
this.operation = operation;
if (operation == "divide" && inputValue === 0) {
throw new Error("Division by zero is not supported");
}
}

public getChildren() {
return [];
}

public getSubqueries(queryASTContext: QueryASTContext<Cypher.Node>): Cypher.Clause[] {
const prop = this.getLeftExpression(queryASTContext);

const bitSize = this.attribute.typeHelper.isInt() ? 32 : 64;
const rightExpr = this.getRightExpression(queryASTContext);
// Avoid overflows, for 64 bit overflows, a long overflow is raised anyway by Neo4j

const maxBit = Cypher.minus(
Cypher.pow(new Cypher.Literal(2), new Cypher.Literal(bitSize - 1)),
new Cypher.Literal(1)
);

return [
Cypher.utils.concat(
Cypher.apoc.util.validate(
Cypher.isNull(prop),
"Cannot %s %s to Nan",
new Cypher.List([new Cypher.Literal(this.operation), this.getParam()])
),
Cypher.apoc.util.validate(
Cypher.gt(rightExpr, maxBit),
"Overflow: Value returned from operator %s is larger than %s bit",
new Cypher.List([new Cypher.Literal(this.operation), new Cypher.Literal(bitSize)])
)
),
];
}

protected getRightExpression(
queryASTContext: QueryASTContext<Cypher.Node>
): Exclude<Cypher.Expr, Cypher.Map | Cypher.MapProjection> {
const rightVariable = super.getRightExpression(queryASTContext);
const targetProperty = this.getLeftExpression(queryASTContext);

switch (this.operation) {
case "add":
case "increment":
return Cypher.plus(targetProperty, rightVariable);
case "decrement":
case "subtract":
return Cypher.minus(targetProperty, rightVariable);
case "divide":
return Cypher.divide(targetProperty, rightVariable);
case "multiply":
return Cypher.multiply(targetProperty, rightVariable);

default:
throw new Error(`Unknown operation ${this.operation}`);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* 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.
*/

import Cypher from "@neo4j/cypher-builder";
import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter";
import { type QueryASTContext } from "../../QueryASTContext";
import { ParamInputField } from "../ParamInputField";

export class PopInputField extends ParamInputField {
constructor({
attribute,
attachedTo,
inputValue,
}: {
attribute: AttributeAdapter;
attachedTo: "node" | "relationship";
inputValue: unknown;
}) {
super({ attribute, attachedTo, inputValue });
}

public getChildren() {
return [];
}

protected getRightExpression(
queryASTContext: QueryASTContext<Cypher.Node>
): Exclude<Cypher.Expr, Cypher.Map | Cypher.MapProjection> {
const rightVariable = super.getParam();
const rightExpr = Cypher.minus(rightVariable);
const leftExpr = this.getLeftExpression(queryASTContext);
return new Cypher.Raw((context) => {
const leftExprCompiled = context.compile(leftExpr);
const poppedValueCompiled = context.compile(rightExpr);
return `${leftExprCompiled}[0..${poppedValueCompiled}]`;
});
}
}
Loading