diff --git a/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.test.ts b/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.test.ts index 40fd77a58..e6ab32e5e 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.test.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.test.ts @@ -8,11 +8,31 @@ describe("Gremlin > edgesSchemaTemplate", () => { expect(normalize(template)).toBe( normalize(` - g.E() - .project("route", "contain") + g.V().limit(1) + .project( + "route", + "contain" + ) .by(V().bothE("route").limit(1)) .by(V().bothE("contain").limit(1)) - .limit(1) + `), + ); + }); + + it("Should deduplicate labels from multi-label types", () => { + const template = edgesSchemaTemplate({ types: ["a::b", "b::c"] }); + + expect(normalize(template)).toBe( + normalize(` + g.V().limit(1) + .project( + "a", + "b", + "c" + ) + .by(V().bothE("a").limit(1)) + .by(V().bothE("b").limit(1)) + .by(V().bothE("c").limit(1)) `), ); }); diff --git a/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.ts b/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.ts index 7c4bcefb3..d2680a307 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchSchema/edgesSchemaTemplate.ts @@ -6,25 +6,31 @@ import { query } from "@/utils"; * Given a set of edge types, it returns a Gremlin template that contains * one sample of each edge type. * - * @example - * types = ["route", "contain"] + * Uses g.V().limit(1) as a dummy anchor because each .by() modulator runs an + * independent global V() sub-traversal that doesn't depend on the anchor value. + * This also avoids Neptune DFE falling back to non-native execution for g.E(). * - * g.E() - * .project("route","contain") - * .by(V().bothE("route").limit(1)) - * .by(V().bothE("contain").limit(1)) - * .limit(1) + * @example + * edgesSchemaTemplate({ types: ["route", "contain"] }) + * // Returns: + * // g.V().limit(1) + * // .project( + * // "route", + * // "contain" + * // ) + * // .by(V().bothE("route").limit(1)) + * // .by(V().bothE("contain").limit(1)) */ export default function edgesSchemaTemplate({ types }: { types: string[] }) { - // Labels with quotes const labels = uniq(types.flatMap(type => type.split("::"))).map( label => `"${label}"`, ); return query` - g.E() - .project(${labels.join(", ")}) + g.V().limit(1) + .project( + ${labels.join(",\n ")} + ) ${labels.map(label => `.by(V().bothE(${label}).limit(1))`).join("\n ")} - .limit(1) `; } diff --git a/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.test.ts b/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.test.ts index 560278ace..1446595d7 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.test.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.test.ts @@ -8,14 +8,31 @@ describe("Gremlin > verticesSchemaTemplate", () => { expect(normalize(template)).toBe( normalize(` - g.V().union( - __.hasLabel("airport").limit(1), - __.hasLabel("country").limit(1) - ) - .fold() - .project("airport", "country") - .by(unfold().hasLabel("airport")) - .by(unfold().hasLabel("country")) + g.V().limit(1) + .project( + "airport", + "country" + ) + .by(V().hasLabel("airport").limit(1)) + .by(V().hasLabel("country").limit(1)) + `), + ); + }); + + it("Should deduplicate labels from multi-label types", () => { + const template = verticesSchemaTemplate({ types: ["a::b", "b::c"] }); + + expect(normalize(template)).toBe( + normalize(` + g.V().limit(1) + .project( + "a", + "b", + "c" + ) + .by(V().hasLabel("a").limit(1)) + .by(V().hasLabel("b").limit(1)) + .by(V().hasLabel("c").limit(1)) `), ); }); diff --git a/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.ts b/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.ts index 4d2b05951..53338394f 100644 --- a/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.ts +++ b/packages/graph-explorer/src/connector/gremlin/fetchSchema/verticesSchemaTemplate.ts @@ -6,36 +6,31 @@ import { query } from "@/utils"; * Given a set of nodes labels, it returns a Gremlin template that contains * one sample of each node label. * - * @example - * types = ["airport", "country"] + * Uses g.V().limit(1) as a dummy anchor because each .by() modulator runs an + * independent global V() sub-traversal that doesn't depend on the anchor value. + * This also avoids Neptune DFE falling back to non-native execution. * - * g.V() - * .union( - * __.hasLabel("airport").limit(1), - * __.hasLabel("country").limit(1) - * ) - * .fold() - * .project( - * "airport", "country" - * ) - * .by(unfold().hasLabel("airport")) - * .by(unfold().hasLabel("country")) + * @example + * verticesSchemaTemplate({ types: ["airport", "country"] }) + * // Returns: + * // g.V().limit(1) + * // .project( + * // "airport", + * // "country" + * // ) + * // .by(V().hasLabel("airport").limit(1)) + * // .by(V().hasLabel("country").limit(1)) */ export default function verticesSchemaTemplate({ types }: { types: string[] }) { - // Labels with quotes const labels = uniq(types.flatMap(type => type.split("::"))).map( label => `"${label}"`, ); return query` - g.V() - .union( - ${labels.map(label => `__.hasLabel(${label}).limit(1)`).join(",\n ")} - ) - .fold() + g.V().limit(1) .project( ${labels.join(",\n ")} ) - ${labels.map(label => `.by(unfold().hasLabel(${label}))`).join("\n ")} - `; + ${labels.map(label => `.by(V().hasLabel(${label}).limit(1))`).join("\n ")} + `; }