From 8d3fd7566b3f7e420b254a9a71d47a955fd823c6 Mon Sep 17 00:00:00 2001 From: Johannes Roith Date: Fri, 24 Nov 2023 14:21:20 +0100 Subject: [PATCH 1/4] feat: experimenting --- .../queryAST/ast/filters/ConnectionFilter.ts | 4 ++++ .../graphql/src/translate/translate-update.ts | 7 ++++++- .../graphql/src/types/neo4j-graphql-context.ts | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts index 879772cc44..17209dc72b 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts @@ -141,6 +141,10 @@ export class ConnectionFilter extends Filter { protected getLabelPredicate(context: QueryASTContext): Cypher.Predicate | undefined { if (!hasTarget(context)) throw new Error("No parent node found!"); if (isConcreteEntity(this.target)) return undefined; + if (context.neo4jGraphQLContext.labelManager) { + const filterExpr = context.neo4jGraphQLContext.labelManager.getLabelSelectorExpression(this.target.name); + return new Cypher.Raw((env) => `${context.target.getCypher(env)}:${filterExpr}`); + } const labelPredicate = this.target.concreteEntities.map((e) => { return context.target.hasLabels(...e.labels); }); diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index c580cc6899..513fcecb13 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -69,7 +69,12 @@ export default async function translateUpdate({ let deleteStr = ""; let projAuth: Cypher.Clause | undefined = undefined; const assumeReconnecting = Boolean(connectInput) && Boolean(disconnectInput); - const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); + + const labels = context.labelManager + ? context.labelManager.getLabelSelectorExpressionObject(node.name) + : node.getLabels(context); + + const matchNode = new Cypher.NamedNode(varName, { labels }); const where = resolveTree.args.where as GraphQLWhereArg | undefined; const topLevelMatch = translateTopLevelMatch({ matchNode, node, context, operation: "UPDATE", where }); matchAndWhereStr = topLevelMatch.cypher; diff --git a/packages/graphql/src/types/neo4j-graphql-context.ts b/packages/graphql/src/types/neo4j-graphql-context.ts index e5bf71f1db..82b80cdab9 100644 --- a/packages/graphql/src/types/neo4j-graphql-context.ts +++ b/packages/graphql/src/types/neo4j-graphql-context.ts @@ -17,11 +17,28 @@ * limitations under the License. */ +import type { LabelExpr } from "@neo4j/cypher-builder"; import type { CypherQueryOptions } from "."; import type { ExecutionContext, Neo4jGraphQLSessionConfig } from "../classes/Executor"; import type { Neo4jGraphQLContextInterface } from "./neo4j-graphql-context-interface"; +type DataModelType = any; // TODO! + +interface LabelSelectorExpressionOptions { + restrict?: DataModelType; +} + +export interface LabelManager { + getLabelSelectorExpressionObject: ( + dataModelType: DataModelType, + options?: LabelSelectorExpressionOptions + ) => LabelExpr | string[]; + getLabelSelectorExpression: (dataModelType: DataModelType, options?: LabelSelectorExpressionOptions) => string; +} + export interface Neo4jGraphQLContext extends Neo4jGraphQLContextInterface { + labelManager?: LabelManager; + /** * Parameters to be used when querying with Cypher. * From a6839dd68164097113a4ef641a7c1a32d20320eb Mon Sep 17 00:00:00 2001 From: Johannes Roith Date: Wed, 24 Jan 2024 13:57:43 +0100 Subject: [PATCH 2/4] fix: improvements --- packages/graphql/package.json | 3 +- .../translate/create-projection-and-params.ts | 131 +++++++++++------- .../queryAST/ast/operations/ReadOperation.ts | 18 ++- .../composite/CompositeReadOperation.ts | 50 +++++-- .../composite/CompositeReadPartial.ts | 38 ++++- .../ast/operations/composite/optimization.ts | 120 ++++++++++++++++ .../ast/operations/optimizationSettings.ts | 5 + .../queryAST/utils/create-node-from-entity.ts | 8 +- .../src/types/neo4j-graphql-context.ts | 5 + yarn.lock | 11 +- 10 files changed, 315 insertions(+), 74 deletions(-) create mode 100644 packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts create mode 100644 packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts diff --git a/packages/graphql/package.json b/packages/graphql/package.json index eaca969b98..7aa8e4f4ef 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -52,6 +52,7 @@ "@apollo/gateway": "2.5.7", "@apollo/server": "4.9.5", "@faker-js/faker": "8.3.1", + "@neo4j/cypher-builder": "../../../cypher-builder", "@types/deep-equal": "1.0.4", "@types/is-uuid": "1.0.2", "@types/jest": "29.5.10", @@ -91,7 +92,6 @@ "@graphql-tools/resolvers-composition": "^7.0.0", "@graphql-tools/schema": "10.0.2", "@graphql-tools/utils": "^10.0.0", - "@neo4j/cypher-builder": "^1.7.1", "camelcase": "^6.3.0", "debug": "^4.3.4", "deep-equal": "^2.0.5", @@ -106,6 +106,7 @@ "uuid": "^9.0.0" }, "peerDependencies": { + "@neo4j/cypher-builder": "^1.7.0", "graphql": "^16.0.0", "neo4j-driver": "^5.8.0" } diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index f643c87562..64ebe64468 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -38,6 +38,8 @@ import { createAuthorizationBeforePredicateField } from "./authorization/create- import { checkAuthentication } from "./authorization/check-authentication"; import { compileCypher } from "../utils/compile-cypher"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; +import { uniqSubQueries } from "./queryAST/ast/operations/composite/optimization"; +import { UNION_UNIFICATION_ENABLED } from "./queryAST/ast/operations/optimizationSettings"; interface Res { projection: Cypher.Expr[]; @@ -141,7 +143,8 @@ export default function createProjectionAndParams({ const subqueryReturnAlias = new Cypher.Variable(); if (relationField.interface || relationField.union) { - let referenceNodes; + let isSelectingAllChildren = false; + let referenceNodes: Node[]; if (relationField.interface) { const interfaceImplementations = context.nodes.filter((x) => relationField.interface?.implementations?.includes(x.name) @@ -172,6 +175,9 @@ export default function createProjectionAndParams({ // where exists and has a filter on this implementation Object.prototype.hasOwnProperty.call(field.args.where, x.name) ); + + isSelectingAllChildren = + relationField.interface && interfaceImplementations.length === referenceNodes.length; } else { referenceNodes = context.nodes.filter( (x) => @@ -182,54 +188,83 @@ export default function createProjectionAndParams({ const parentNode = varName; - const unionSubqueries: Cypher.Clause[] = []; - for (const refNode of referenceNodes) { - const targetNode = new Cypher.Node({ labels: refNode.getLabels(context) }); - const recurse = createProjectionAndParams({ - resolveTree: field, - node: refNode, - context, - varName: targetNode, - cypherFieldAliasMap, - }); - res.params = { ...res.params, ...recurse.params }; - - const direction = getCypherRelationshipDirection(relationField, field.args); - - const nestedProjection = new Cypher.RawCypher((env) => { - // The nested projection will be surrounded by brackets, so we want to remove - // any linebreaks, and then the first opening and the last closing bracket of the line, - // as well as any surrounding whitespace. - const nestedProj = compileCypher(recurse.projection, env).replaceAll( - /(^\s*{\s*)|(\s*}\s*$)/g, - "" - ); + // TODO: why is it important if we match all children? + // -> because otherwise we already need a partial exclusion predicate! + const matchByInterfaceOrUnion = + UNION_UNIFICATION_ENABLED && isSelectingAllChildren + ? relationField.interface?.typeMeta.name + : undefined; - return `{ __resolveType: "${refNode.name}", __id: id(${compileCypher(varName, env)})${ - nestedProj && `, ${nestedProj}` - } }`; - }); - - const subquery = createProjectionSubquery({ - parentNode, - whereInput: field.args.where ? field.args.where[refNode.name] : {}, - node: refNode, - context, - subqueryReturnAlias, - nestedProjection, - nestedSubqueries: [...recurse.subqueriesBeforeSort, ...recurse.subqueries], - targetNode, - relationField, - relationshipDirection: direction, - optionsInput, - addSkipAndLimit: false, - collect: false, - nestedPredicates: recurse.predicates, - }); - - const unionWith = new Cypher.With("*"); - unionSubqueries.push(Cypher.concat(unionWith, subquery)); - } + const unionSubqueries = uniqSubQueries( + context, + matchByInterfaceOrUnion, + referenceNodes, + (node) => node, + (subs) => { + const unionSubqueries: Cypher.Clause[][] = []; + for (const { child: refNode, unifyViaDataModelType, exclusionPredicates } of subs) { + const labels = + unifyViaDataModelType && context.labelManager + ? context.labelManager.getLabelSelectorExpressionObject(matchByInterfaceOrUnion) + : refNode.getLabels(context); + + const targetNode = new Cypher.Node({ labels }); + const recurse = createProjectionAndParams({ + resolveTree: field, + node: refNode, + context, + varName: targetNode, + cypherFieldAliasMap, + }); + + res.params = { ...res.params, ...recurse.params }; + + const combinedPredicates = [ + ...(recurse.predicates ?? []), + ...(exclusionPredicates?.(targetNode) ?? []), + ]; + + const direction = getCypherRelationshipDirection(relationField, field.args); + + const nestedProjection = new Cypher.RawCypher((env) => { + // The nested projection will be surrounded by brackets, so we want to remove + // any linebreaks, and then the first opening and the last closing bracket of the line, + // as well as any surrounding whitespace. + const nestedProj = compileCypher(recurse.projection, env).replaceAll( + /(^\s*{\s*)|(\s*}\s*$)/g, + "" + ); + + return `{ __resolveType: ${ + UNION_UNIFICATION_ENABLED && context.labelManager?.hasMainType(refNode.name) + ? new Cypher.Property(targetNode, "mainType").getCypher(env) + : `"${refNode.name}"` + }, __id: id(${compileCypher(varName, env)})${nestedProj && `, ${nestedProj}`} }`; + }); + + const subquery = createProjectionSubquery({ + parentNode, + whereInput: field.args.where ? field.args.where[refNode.name] : {}, + node: refNode, + context, + subqueryReturnAlias, + nestedProjection, + nestedSubqueries: [...recurse.subqueriesBeforeSort, ...recurse.subqueries], + targetNode, + relationField, + relationshipDirection: direction, + optionsInput, + addSkipAndLimit: false, + collect: false, + nestedPredicates: combinedPredicates, + }); + + const unionWith = new Cypher.With("*"); + unionSubqueries.push([Cypher.concat(unionWith, subquery)]); + } + return unionSubqueries; + } + ).flat(); const unionClause = new Cypher.Union(...unionSubqueries); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts index 782e7623d0..fa5e3724cc 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts @@ -35,6 +35,7 @@ import { CypherPropertySort } from "../sort/CypherPropertySort"; import type { Sort } from "../sort/Sort"; import type { OperationTranspileResult } from "./operations"; import { Operation } from "./operations"; +import { READ_LOWER_TARGET_INTERFACE_ENABLED } from "./optimizationSettings"; export class ReadOperation extends Operation { public readonly target: ConcreteEntityAdapter; @@ -103,7 +104,22 @@ export class ReadOperation extends Operation { //TODO: dupe from transpile if (!hasTarget(context)) throw new Error("No parent node found!"); const relVar = createRelationshipFromEntity(entity); - const targetNode = createNodeFromEntity(entity.target, context.neo4jGraphQLContext); + + const lowerToTargetType = READ_LOWER_TARGET_INTERFACE_ENABLED + ? context.neo4jGraphQLContext.labelManager?.getLowerTargetInterfaceIfSafeRelationship( + entity.source.name, + entity.name + ) + : null; + + const targetNode = + lowerToTargetType && context.neo4jGraphQLContext.labelManager + ? new Cypher.Node({ + labels: context.neo4jGraphQLContext.labelManager.getLabelSelectorExpressionObject( + lowerToTargetType + ), + }) + : createNodeFromEntity(entity.target, context.neo4jGraphQLContext); const relDirection = entity.getCypherDirection(this.directed); const pattern = new Cypher.Pattern(context.target) diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts index ca9656ef2a..eab9355092 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts @@ -28,6 +28,7 @@ import type { RelationshipAdapter } from "../../../../../schema-model/relationsh import type { Pagination } from "../../pagination/Pagination"; import type { Sort, SortField } from "../../sort/Sort"; import type { QueryASTContext } from "../../QueryASTContext"; +import { uniqSubQueries } from "./optimization"; export class CompositeReadOperation extends Operation { private children: CompositeReadPartial[]; @@ -56,10 +57,25 @@ export class CompositeReadOperation extends Operation { } private transpileTopLevelCompositeRead(context: QueryASTContext): OperationTranspileResult { - const nestedSubqueries = this.children.flatMap((c) => { - const result = c.transpile(context); - return result.clauses; - }); + //const nestedSubqueries = this.children.flatMap((c) => { + // const result = c.transpile(context); + // return result.clauses; + //}); + + const isSelectingAllChildren = this.entity.concreteEntities.length === this.children.length; + const matchByInterfaceOrUnion = isSelectingAllChildren ? this.entity.name : undefined; + const nestedSubqueries = uniqSubQueries( + context.neo4jGraphQLContext, + matchByInterfaceOrUnion, + this.children, + (child) => child.target, + (subs) => + subs.map(({ child, unifyViaDataModelType, exclusionPredicates }) => { + const result = child.transpile(context, unifyViaDataModelType, exclusionPredicates); + return result.clauses; + }) + ).flat(); + const nestedSubquery = new Cypher.Call(new Cypher.Union(...nestedSubqueries)).return(context.returnVariable); if (this.sortFields.length > 0) { nestedSubquery.orderBy(...this.getSortFields(context, context.returnVariable)); @@ -87,15 +103,25 @@ export class CompositeReadOperation extends Operation { } const parentNode = context.target; - const nestedSubqueries = this.children.flatMap((c) => { - const result = c.transpile(context); - let clauses = result.clauses; - if (parentNode) { - clauses = clauses.map((sq) => Cypher.concat(new Cypher.With("*"), sq)); - } - return clauses; - }); + const isSelectingAllChildren = this.entity.concreteEntities.length === this.children.length; + const matchByInterfaceOrUnion = isSelectingAllChildren ? this.entity.name : undefined; + const nestedSubqueries = uniqSubQueries( + context.neo4jGraphQLContext, + matchByInterfaceOrUnion, + this.children, + (child) => child.target, + (subs) => + subs.map(({ child, unifyViaDataModelType, exclusionPredicates }) => { + const result = child.transpile(context, unifyViaDataModelType, exclusionPredicates); + + let clauses = result.clauses; + if (parentNode) { + clauses = clauses.map((sq) => Cypher.concat(new Cypher.With("*"), sq)); + } + return clauses; + }) + ).flat(); let aggrExpr: Cypher.Expr = Cypher.collect(context.returnVariable); if (this.relationship && !this.relationship.isList) { diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts index 96e022b8b9..d0f2523611 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts @@ -24,11 +24,21 @@ import { createNodeFromEntity, createRelationshipFromEntity } from "../../../uti import { QueryASTContext } from "../../QueryASTContext"; import { ReadOperation } from "../ReadOperation"; import type { OperationTranspileResult } from "../operations"; +import { UNION_UNIFICATION_ENABLED } from "../optimizationSettings"; export class CompositeReadPartial extends ReadOperation { - public transpile(context: QueryASTContext) { + public transpile( + context: QueryASTContext, + matchByInterfaceOrUnion?: string, + exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[] + ) { if (this.relationship) { - return this.transpileNestedCompositeRelationship(this.relationship, context); + return this.transpileNestedCompositeRelationship( + this.relationship, + context, + matchByInterfaceOrUnion, + exclusionPredicates + ); } else { return this.transpileTopLevelCompositeEntity(context); } @@ -36,12 +46,21 @@ export class CompositeReadPartial extends ReadOperation { private transpileNestedCompositeRelationship( entity: RelationshipAdapter, - context: QueryASTContext + context: QueryASTContext, + matchByInterfaceOrUnion?: string, + exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[] ): OperationTranspileResult { if (!hasTarget(context)) throw new Error("No parent node found!"); const parentNode = context.target; const relVar = createRelationshipFromEntity(entity); - const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext); + const targetNode = + matchByInterfaceOrUnion && context.neo4jGraphQLContext.labelManager + ? new Cypher.Node({ + labels: context.neo4jGraphQLContext.labelManager.getLabelSelectorExpressionObject( + matchByInterfaceOrUnion + ), + }) + : createNodeFromEntity(this.target, context.neo4jGraphQLContext); const relDirection = entity.getCypherDirection(this.directed); const pattern = new Cypher.Pattern(parentNode) @@ -56,7 +75,11 @@ export class CompositeReadPartial extends ReadOperation { const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext); const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); - const wherePredicate = Cypher.and(filterPredicates, ...authFiltersPredicate); + const wherePredicate = Cypher.and( + filterPredicates, + ...authFiltersPredicate, + ...(exclusionPredicates?.(targetNode) ?? []) + ); if (wherePredicate) { // NOTE: This is slightly different to ReadOperation for cypher compatibility, this could use `WITH *` matchClause.where(wherePredicate); @@ -119,7 +142,10 @@ export class CompositeReadPartial extends ReadOperation { const targetNodeName = this.target.name; projection.set({ - __resolveType: new Cypher.Literal(targetNodeName), + __resolveType: + UNION_UNIFICATION_ENABLED && context.neo4jGraphQLContext.labelManager?.hasMainType(targetNodeName) + ? new Cypher.Property(context.target, "mainType") + : new Cypher.Literal(targetNodeName), __id: Cypher.id(context.target), }); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts new file mode 100644 index 0000000000..4724441783 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/optimization.ts @@ -0,0 +1,120 @@ +import Cypher from "@neo4j/cypher-builder"; +import { groupBy, uniq, compact, zip } from "lodash"; +import { UNION_UNIFICATION_ENABLED } from "../optimizationSettings"; +import type { Neo4jGraphQLTranslationContext } from "../../../../../types/neo4j-graphql-translation-context"; + +function getMilliSeconds(hrTime: [number, number]) { + return (hrTime[0] * 1000000 + hrTime[1] / 1000.0) / 1000.0; +} + +interface UniqSubQueriesEntry { + child: T; + unifyViaDataModelType?: string; + exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[]; +} + +interface Target { + name: string; + getLabels: (...args: any[]) => string[]; +} + +// +// This eliminiates sub-queries if they are EXACTLY equal. +// Note that we recursively calling this function, so basically we repeatedly +// compact the tree right to left and then top to bottom. +// Furthermore after compaction we need to recompute with different selectors. +// +// This is obviously not very efficient - but it's easy and robust. +// +export function uniqSubQueries( + context: Neo4jGraphQLTranslationContext, + matchUsingInterfaceOrUnion: string | undefined, + children: T[], + targetExtractor: (t: T) => Target, + nestedSubqueriesProducer: (subs: UniqSubQueriesEntry[]) => Cypher.Clause[][] +): Cypher.Clause[][] { + if (!UNION_UNIFICATION_ENABLED || !matchUsingInterfaceOrUnion || children.length < 2) { + return nestedSubqueriesProducer(children.map((c) => ({ child: c }))); + } + + const hrStart = process.hrtime(); + + // TODO: This way of doing things is really slow! - around 50ms for a realistic query. + // It seems like the most expensive part is building the subquery twice! + + const nestedSubqueries = nestedSubqueriesProducer( + children.map((c) => ({ child: c, unifyViaDataModelType: matchUsingInterfaceOrUnion })) + ); + + const nestedSubqueriesWithChildren = zip(nestedSubqueries, children); + + const groupedChildren = compact( + uniq( + Object.values( + groupBy(nestedSubqueriesWithChildren, ([sqLst, child]) => { + if (!child) { + throw new Error("optimizer: child missing. should not happen."); + } + if (!sqLst) { + throw new Error("optimizer: sqLst missing. should not happen."); + } + return sqLst + .map((sq) => { + const { cypher, params } = sq.build(); + // console.log(">>> hopefully similiar parts: ", cypher); + if (Object.keys(params).length > 0) { + return `${cypher}:${JSON.stringify(params)}`; // group by query. + } + return cypher; // group by query. + }) + .join(";"); + }) + ).map((group) => compact(group.map((g) => g[1]))) + ) + ); + + const { labelManager } = context; + + function computeExclusionLabels(groupedChildren: T[][], g: T[]): (Cypher.LabelExpr | string[])[] { + const labels = groupedChildren + .filter((cur) => cur !== g) + .flatMap((cur) => + cur.map((it) => { + const target = targetExtractor(it); + console.log(">>> BUILDING exclusion label for " + target.name); + if (labelManager) { + return labelManager.getLabelSelectorExpressionObject(target.name); + } + return target.getLabels(); + }) + ); + return labels; + } + + const res = groupedChildren.map((g) => ({ + child: g[0] as T, + ...(g.length > 1 + ? ({ + unifyViaDataModelType: matchUsingInterfaceOrUnion, + exclusionPredicates: (matchNode) => { + const exclusions = computeExclusionLabels(groupedChildren, g); + if (exclusions.length > 0) { + return [ + Cypher.not( + Cypher.or( + ...exclusions.map((orGroup) => Cypher.and(matchNode.hasLabelsOf(orGroup, true))) + ) + ), + ]; + } + return []; + }, + } satisfies Partial>) + : {}), + })); + + const time = getMilliSeconds(process.hrtime(hrStart)); + console.log(">>> optimization took (ms): ", time); + + return nestedSubqueriesProducer(res); +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts b/packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts new file mode 100644 index 0000000000..3fdb0c7d89 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/optimizationSettings.ts @@ -0,0 +1,5 @@ +export const UNION_UNIFICATION_ENABLED = process.env.OPTIMIZE_UNIONS !== "0"; +export const READ_LOWER_TARGET_INTERFACE_ENABLED = process.env.OPTIMIZE_TARGET_INTERFACE !== "0"; + +console.log(">>> UNION_UNIFICATION_ENABLED: ", UNION_UNIFICATION_ENABLED); +console.log(">>> READ_LOWER_TARGET_INTERFACE_ENABLED: ", READ_LOWER_TARGET_INTERFACE_ENABLED); diff --git a/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts b/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts index e7cdcb6678..723134147e 100644 --- a/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts +++ b/packages/graphql/src/translate/queryAST/utils/create-node-from-entity.ts @@ -30,7 +30,13 @@ export function createNodeFromEntity( name?: string ): Cypher.Node { const nodeLabels = entity instanceof ConcreteEntityAdapter ? entity.getLabels() : [entity.name]; - const labels = neo4jGraphQLContext ? mapLabelsWithContext(nodeLabels, neo4jGraphQLContext) : nodeLabels; + const hasContextLabels = nodeLabels.some((l) => l.startsWith("$")); + const _labels = neo4jGraphQLContext ? mapLabelsWithContext(nodeLabels, neo4jGraphQLContext) : nodeLabels; + + const labels = + hasContextLabels || !neo4jGraphQLContext?.labelManager + ? _labels + : neo4jGraphQLContext.labelManager.getLabelSelectorExpressionObject(entity.name); if (name) { return new Cypher.NamedNode(name, { labels }); diff --git a/packages/graphql/src/types/neo4j-graphql-context.ts b/packages/graphql/src/types/neo4j-graphql-context.ts index 82b80cdab9..26b51f8a4d 100644 --- a/packages/graphql/src/types/neo4j-graphql-context.ts +++ b/packages/graphql/src/types/neo4j-graphql-context.ts @@ -29,6 +29,11 @@ interface LabelSelectorExpressionOptions { } export interface LabelManager { + getLowerTargetInterfaceIfSafeRelationship: ( + dataModelType: DataModelType, + fieldName: string + ) => DataModelType | null; + hasMainType: (dataModelType: DataModelType) => boolean; getLabelSelectorExpressionObject: ( dataModelType: DataModelType, options?: LabelSelectorExpressionOptions diff --git a/yarn.lock b/yarn.lock index 2e317206df..090d812c0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3556,10 +3556,10 @@ __metadata: languageName: node linkType: hard -"@neo4j/cypher-builder@npm:^1.7.1": - version: 1.7.1 - resolution: "@neo4j/cypher-builder@npm:1.7.1" - checksum: 545953bffec407c65ac3531868e8853376ecf254dfa15cfabb4e929417d643164147c0bfd3bb28fdb37521bfc8573842d441d947513bf58c516287da133bf2c2 +"@neo4j/cypher-builder@file:../../../cypher-builder::locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql": + version: 1.7.4 + resolution: "@neo4j/cypher-builder@file:../../../cypher-builder#../../../cypher-builder::hash=abed31&locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql" + checksum: e416f3d75d9bffaecf03766e7551cde966b7ae6060b19c16318803d5c6adf5be19e65628d6a4187f5cdced7732924b3020188829015925627e7aa5f91097b351 languageName: node linkType: hard @@ -3705,7 +3705,7 @@ __metadata: "@graphql-tools/resolvers-composition": ^7.0.0 "@graphql-tools/schema": 10.0.2 "@graphql-tools/utils": ^10.0.0 - "@neo4j/cypher-builder": ^1.7.1 + "@neo4j/cypher-builder": ../../../cypher-builder "@types/deep-equal": 1.0.4 "@types/is-uuid": 1.0.2 "@types/jest": 29.5.10 @@ -3751,6 +3751,7 @@ __metadata: uuid: ^9.0.0 ws: 8.14.2 peerDependencies: + "@neo4j/cypher-builder": ^1.7.0 graphql: ^16.0.0 neo4j-driver: ^5.8.0 languageName: unknown From ac2f73e5c8febc2b512fced766cee2421be35db1 Mon Sep 17 00:00:00 2001 From: Johannes Roith Date: Wed, 24 Jan 2024 16:18:59 +0100 Subject: [PATCH 3/4] fix: merge --- .../composite/CompositeReadPartial.ts | 12 +++--------- .../queryAST/ast/selection/EntitySelection.ts | 5 ++++- .../ast/selection/RelationshipSelection.ts | 18 +++++++++++------- yarn.lock | 4 ++-- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts index 0e6503702c..c7d1779656 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts @@ -33,19 +33,13 @@ export class CompositeReadPartial extends ReadOperation { exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[] ) { if (this.relationship) { - return this.transpileNestedCompositeRelationship( - this.relationship, - context, - matchByInterfaceOrUnion, - exclusionPredicates - ); + return this.transpileNestedCompositeRelationship(context, matchByInterfaceOrUnion, exclusionPredicates); } else { return this.transpileTopLevelCompositeEntity(context); } } private transpileNestedCompositeRelationship( - entity: RelationshipAdapter, context: QueryASTContext, matchByInterfaceOrUnion?: string, exclusionPredicates?: (matchNode: Cypher.Node) => Cypher.Predicate[] @@ -53,7 +47,7 @@ export class CompositeReadPartial extends ReadOperation { if (!hasTarget(context)) throw new Error("No parent node found!"); // eslint-disable-next-line prefer-const - let { selection: matchClause, nestedContext } = this.selection.apply(context); + let { selection: matchClause, nestedContext } = this.selection.apply(context, matchByInterfaceOrUnion); let extraMatches: SelectionClause[] = this.getChildren().flatMap((f) => { return f.getSelection(nestedContext); @@ -71,7 +65,7 @@ export class CompositeReadPartial extends ReadOperation { const wherePredicate = Cypher.and( filterPredicates, ...authFiltersPredicate, - ...(exclusionPredicates?.(targetNode) ?? []) + ...(exclusionPredicates?.(nestedContext.target) ?? []) ); if (wherePredicate) { // NOTE: This is slightly different to ReadOperation for cypher compatibility, this could use `WITH *` diff --git a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts index 7afbf0f8a9..d272a7202d 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts @@ -30,7 +30,10 @@ export abstract class EntitySelection extends QueryASTNode { /** Apply selection over the given context, returns the updated context and the selection clause * TODO: Improve naming */ - public abstract apply(context: QueryASTContext): { + public abstract apply( + context: QueryASTContext, + matchByInterfaceOrUnion?: string + ): { nestedContext: QueryASTContext; selection: SelectionClause; }; diff --git a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts index d53c855445..3f8a6b80ba 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts @@ -55,7 +55,10 @@ export class RelationshipSelection extends EntitySelection { this.optional = optional ?? false; } - public apply(context: QueryASTContext): { + public apply( + context: QueryASTContext, + matchByInterfaceOrUnion?: string + ): { nestedContext: QueryASTContext; selection: SelectionClause; } { @@ -65,12 +68,13 @@ export class RelationshipSelection extends EntitySelection { const relationshipTarget = this.targetOverride ?? this.relationship.target; const relDirection = this.relationship.getCypherDirection(this.directed); - const lowerToTargetType = READ_LOWER_TARGET_INTERFACE_ENABLED - ? context.neo4jGraphQLContext.labelManager?.getLowerTargetInterfaceIfSafeRelationship( - this.relationship.source.name, - this.relationship.name - ) - : null; + const lowerToTargetType = + matchByInterfaceOrUnion ?? READ_LOWER_TARGET_INTERFACE_ENABLED + ? context.neo4jGraphQLContext.labelManager?.getLowerTargetInterfaceIfSafeRelationship( + this.relationship.source.name, + this.relationship.name + ) + : null; const targetNode = lowerToTargetType && context.neo4jGraphQLContext.labelManager diff --git a/yarn.lock b/yarn.lock index 0d41ef0454..e2f3b440d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3568,8 +3568,8 @@ __metadata: "@neo4j/cypher-builder@file:../../../cypher-builder::locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql": version: 1.7.4 - resolution: "@neo4j/cypher-builder@file:../../../cypher-builder#../../../cypher-builder::hash=abed31&locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql" - checksum: e416f3d75d9bffaecf03766e7551cde966b7ae6060b19c16318803d5c6adf5be19e65628d6a4187f5cdced7732924b3020188829015925627e7aa5f91097b351 + resolution: "@neo4j/cypher-builder@file:../../../cypher-builder#../../../cypher-builder::hash=630c22&locator=%40neo4j%2Fgraphql%40workspace%3Apackages%2Fgraphql" + checksum: cd5bb11ac447ea45c200e1c9c747e12cba245ea0f780796398a91894bc66b4b0618e2edc485bd4bd7bc5b5193523147f91e63bdcbfdf0e623e552f7971c8fe62 languageName: node linkType: hard From 5e523538998b283eac7f3630ca16cf5e6479b017 Mon Sep 17 00:00:00 2001 From: Johannes Roith Date: Wed, 24 Jan 2024 16:55:11 +0100 Subject: [PATCH 4/4] fix: bug --- .../translate/create-projection-and-params.ts | 4 +- .../composite/CompositeReadOperation.ts | 41 ------------------- .../ast/selection/RelationshipSelection.ts | 5 ++- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index 7cc9a20098..8f0ef62828 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -188,8 +188,6 @@ export default function createProjectionAndParams({ const parentNode = varName; - // TODO: why is it important if we match all children? - // -> because otherwise we already need a partial exclusion predicate! const matchByInterfaceOrUnion = UNION_UNIFICATION_ENABLED && isSelectingAllChildren ? relationField.interface?.typeMeta.name @@ -205,7 +203,7 @@ export default function createProjectionAndParams({ for (const { child: refNode, unifyViaDataModelType, exclusionPredicates } of subs) { const labels = unifyViaDataModelType && context.labelManager - ? context.labelManager.getLabelSelectorExpressionObject(matchByInterfaceOrUnion) + ? context.labelManager.getLabelSelectorExpressionObject(unifyViaDataModelType) : refNode.getLabels(context); const targetNode = new Cypher.Node({ labels }); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts index be172fbba5..c71fbc9494 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts @@ -56,47 +56,6 @@ export class CompositeReadOperation extends Operation { return this.children; } - private transpileTopLevelCompositeRead(context: QueryASTContext): OperationTranspileResult { - //const nestedSubqueries = this.children.flatMap((c) => { - // const result = c.transpile(context); - // return result.clauses; - //}); - - const isSelectingAllChildren = this.entity.concreteEntities.length === this.children.length; - const matchByInterfaceOrUnion = isSelectingAllChildren ? this.entity.name : undefined; - const nestedSubqueries = uniqSubQueries( - context.neo4jGraphQLContext, - matchByInterfaceOrUnion, - this.children, - (child) => child.target, - (subs) => - subs.map(({ child, unifyViaDataModelType, exclusionPredicates }) => { - const result = child.transpile(context, unifyViaDataModelType, exclusionPredicates); - return result.clauses; - }) - ).flat(); - - const nestedSubquery = new Cypher.Call(new Cypher.Union(...nestedSubqueries)).return(context.returnVariable); - if (this.sortFields.length > 0) { - nestedSubquery.orderBy(...this.getSortFields(context, context.returnVariable)); - } - if (this.pagination) { - const paginationField = this.pagination.getPagination(); - if (paginationField) { - if (paginationField.skip) { - nestedSubquery.skip(paginationField.skip); - } - if (paginationField.limit) { - nestedSubquery.limit(paginationField.limit); - } - } - } - return { - clauses: [nestedSubquery], - projectionExpr: context.returnVariable, - }; - } - public transpile(context: QueryASTContext): OperationTranspileResult { const parentNode = context.target; diff --git a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts index 3f8a6b80ba..2d168c98ce 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts @@ -69,12 +69,13 @@ export class RelationshipSelection extends EntitySelection { const relDirection = this.relationship.getCypherDirection(this.directed); const lowerToTargetType = - matchByInterfaceOrUnion ?? READ_LOWER_TARGET_INTERFACE_ENABLED + matchByInterfaceOrUnion ?? + (READ_LOWER_TARGET_INTERFACE_ENABLED ? context.neo4jGraphQLContext.labelManager?.getLowerTargetInterfaceIfSafeRelationship( this.relationship.source.name, this.relationship.name ) - : null; + : null); const targetNode = lowerToTargetType && context.neo4jGraphQLContext.labelManager