blob: 70a3598e2c7047ca5badf4216d906e17bdcc6de0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 { P, TextP, t, direction } from '../traversal.js';
import { Graph, Vertex, Edge, VertexProperty, Property, Path } from '../../structure/graph.js';
import type { Arg, Pipeline, ExecutionContext, RepeatSpec } from './types.js';
// ── Traverser helpers ─────────────────────────────────────────────────────────
export type StreamItem = any;
export function wrap(value: any, prevPath: PathEntry[], trackPaths: boolean): StreamItem {
if (!trackPaths) return value;
return { value, path: [...prevPath, { labels: [], object: value }] };
}
export function getValue(item: StreamItem, trackPaths: boolean): any {
return trackPaths ? item.value : item;
}
export function getPath(item: StreamItem, trackPaths: boolean): PathEntry[] {
return trackPaths ? (item.path ?? []) : [];
}
export interface PathEntry {
labels: string[];
object: any;
}
// ── Property lookup helpers ───────────────────────────────────────────────────
/** Returns the property key regardless of whether it's a Property or VertexProperty. */
function propKey(p: Property | VertexProperty): string {
return p instanceof VertexProperty ? p.label : p.key;
}
/** Returns all properties of an element as a flat list. */
function allProps(el: Vertex | Edge): Array<Property | VertexProperty> {
return (el.properties ?? []) as Array<Property | VertexProperty>;
}
/** Returns all values for a given key on an element (vertex properties are multi-valued). */
function getValues(el: Vertex | Edge, key: string): any[] {
return allProps(el)
.filter(p => propKey(p) === key)
.map(p => (p as any).value);
}
/** Returns true when the element has at least one property with the given key. */
function hasKey(el: Vertex | Edge, key: string): boolean {
return allProps(el).some(p => propKey(p) === key);
}
// ── Predicate evaluation ──────────────────────────────────────────────────────
export function evaluatePredicate(pred: P | TextP, value: any): boolean {
const op = pred.operator;
const v = pred.value;
const other = pred.other;
switch (op) {
case 'eq': { const c = compareForPredicate(value, v); return c !== null && c === 0; }
case 'neq': { const c = compareForPredicate(value, v); return c !== null ? c !== 0 : value !== v; }
case 'lt': { const c = compareForPredicate(value, v); return c !== null && c < 0; }
case 'lte': { const c = compareForPredicate(value, v); return c !== null && c <= 0; }
case 'gt': { const c = compareForPredicate(value, v); return c !== null && c > 0; }
case 'gte': { const c = compareForPredicate(value, v); return c !== null && c >= 0; }
case 'between': {
const clo = compareForPredicate(value, v);
const chi = compareForPredicate(value, other);
return clo !== null && chi !== null && clo >= 0 && chi < 0;
}
case 'inside': {
const clo = compareForPredicate(value, v);
const chi = compareForPredicate(value, other);
return clo !== null && chi !== null && clo > 0 && chi < 0;
}
case 'outside': {
const clo = compareForPredicate(value, v);
const chi = compareForPredicate(value, other);
return (clo !== null && clo < 0) || (chi !== null && chi > 0);
}
case 'within': return (Array.isArray(v) ? v : [v]).includes(value);
case 'without': return !(Array.isArray(v) ? v : [v]).includes(value);
case 'not': return !evaluatePredicate(v as P, value);
case 'and': return evaluatePredicate(v as P, value) && evaluatePredicate(other as P, value);
case 'or': return evaluatePredicate(v as P, value) || evaluatePredicate(other as P, value);
case 'containing': return typeof value === 'string' && value.includes(v as string);
case 'notContaining': return typeof value === 'string' && !value.includes(v as string);
case 'startingWith': return typeof value === 'string' && value.startsWith(v as string);
case 'notStartingWith': return typeof value === 'string' && !value.startsWith(v as string);
case 'endingWith': return typeof value === 'string' && value.endsWith(v as string);
case 'notEndingWith': return typeof value === 'string' && !value.endsWith(v as string);
case 'regex': return typeof value === 'string' && new RegExp(v as string, 'u').test(value);
case 'notRegex': return typeof value === 'string' && !new RegExp(v as string, 'u').test(value);
default: return false;
}
}
// ── Navigation steps ──────────────────────────────────────────────────────────
export function* stepOut(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const labels = args as string[];
for (const item of source) {
const v: Vertex = getValue(item, trackPaths);
const edges = graph.outEdges.get(v.id) ?? [];
for (const e of edges) {
if (labels.length === 0 || labels.includes(e.label)) {
yield wrap(e.inV, getPath(item, trackPaths), trackPaths);
}
}
}
}
export function* stepIn(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const labels = args as string[];
for (const item of source) {
const v: Vertex = getValue(item, trackPaths);
const edges = graph.inEdges.get(v.id) ?? [];
for (const e of edges) {
if (labels.length === 0 || labels.includes(e.label)) {
yield wrap(e.outV, getPath(item, trackPaths), trackPaths);
}
}
}
}
export function* stepBoth(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const buf = [...source];
yield* stepOut(buf, args, graph, trackPaths);
yield* stepIn(buf, args, graph, trackPaths);
}
export function* stepOutE(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const labels = args as string[];
for (const item of source) {
const v: Vertex = getValue(item, trackPaths);
const edges = graph.outEdges.get(v.id) ?? [];
for (const e of edges) {
if (labels.length === 0 || labels.includes(e.label)) {
yield wrap(e, getPath(item, trackPaths), trackPaths);
}
}
}
}
export function* stepInE(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const labels = args as string[];
for (const item of source) {
const v: Vertex = getValue(item, trackPaths);
const edges = graph.inEdges.get(v.id) ?? [];
for (const e of edges) {
if (labels.length === 0 || labels.includes(e.label)) {
yield wrap(e, getPath(item, trackPaths), trackPaths);
}
}
}
}
export function* stepBothE(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const buf = [...source]; // already buffered correctly
yield* stepOutE(buf, args, graph, trackPaths);
yield* stepInE(buf, args, graph, trackPaths);
}
export function* stepOutV(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const e: Edge = getValue(item, trackPaths);
yield wrap(e.outV, getPath(item, trackPaths), trackPaths);
}
}
export function* stepInV(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const e: Edge = getValue(item, trackPaths);
yield wrap(e.inV, getPath(item, trackPaths), trackPaths);
}
}
export function* stepOtherV(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
// trackPaths is always true when otherV is present (forced in LocalExecutor)
for (const item of source) {
const e: Edge = getValue(item, trackPaths);
const path = getPath(item, trackPaths);
const prevEntry = path.length >= 2 ? path[path.length - 2] : null;
const prevId = prevEntry?.object?.id;
const result = prevId === e.outV.id ? e.inV : e.outV;
yield wrap(result, path, trackPaths);
}
}
// ── Filter steps ──────────────────────────────────────────────────────────────
export function* stepHas(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const el = getValue(item, trackPaths);
if (matchesHas(el, args)) yield item;
}
}
function matchesHas(el: any, args: Arg[]): boolean {
if (!(el instanceof Vertex || el instanceof Edge)) return false;
if (args.length === 1) {
const key = args[0] as string;
if (key === 'T.id' || key === 'id') return true;
if (key === 'T.label' || key === 'label') return true;
return hasKey(el, key);
}
if (args.length === 2) {
const [key, valOrPred] = args;
const keyStr = key as string;
let values: any[];
if (keyStr === 'T.id' || keyStr === 'id') {
values = [el.id];
} else if (keyStr === 'T.label' || keyStr === 'label') {
values = [el.label];
} else {
values = getValues(el, keyStr);
}
if (values.length === 0) return false;
if (valOrPred instanceof P || valOrPred instanceof TextP) {
return values.some(v => evaluatePredicate(valOrPred, v));
}
return values.some(v => v === valOrPred);
}
if (args.length === 3) {
const [label, key, valOrPred] = args;
if (el.label !== label) return false;
const values = getValues(el, key as string);
if (values.length === 0) return false;
if (valOrPred instanceof P || valOrPred instanceof TextP) {
return values.some(v => evaluatePredicate(valOrPred as P, v));
}
return values.some(v => v === valOrPred);
}
return false;
}
function idMatches(id: any, argId: Arg): boolean {
if (argId === null || argId === undefined) return false;
return id === argId;
}
function flattenIdArgs(args: Arg[]): Arg[] {
// Single list argument: unroll it, consistent with hasId([1,2,3]) semantics.
// Multiple arguments: treat each as-is — a list is a single ID value, not unrolled.
if (args.length === 1 && Array.isArray(args[0])) return args[0] as unknown as Arg[];
return args;
}
export function* stepHasId(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const flatArgs = flattenIdArgs(args);
for (const item of source) {
const el = getValue(item, trackPaths);
const id = el?.id;
if (flatArgs.length > 0 && flatArgs[0] instanceof P) {
if (evaluatePredicate(flatArgs[0] as P, id)) yield item;
} else if (flatArgs.some(a => idMatches(id, a))) {
yield item;
}
}
}
export function* stepHasLabel(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const el = getValue(item, trackPaths);
const label = el?.label;
if (args.length > 0 && args[0] instanceof P) {
if (evaluatePredicate(args[0] as P, label)) yield item;
} else if (args.length === 0 || args.includes(label)) {
yield item;
}
}
}
export function* stepHasNot(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const key = args[0] as string;
for (const item of source) {
const el = getValue(item, trackPaths);
if (el instanceof Vertex || el instanceof Edge) {
if (!hasKey(el, key)) yield item;
}
}
}
export function* stepIs(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const arg = args[0];
for (const item of source) {
const v = getValue(item, trackPaths);
if (arg instanceof P || arg instanceof TextP) {
if (evaluatePredicate(arg, v)) yield item;
} else if (v === arg) {
yield item;
}
}
}
export function* stepHasKey(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const p = getValue(item, trackPaths);
const k = propKey(p);
if (args.length === 0) {
yield item;
} else if (args[0] instanceof P) {
if (evaluatePredicate(args[0] as P, k)) yield item;
} else if (args.includes(k)) {
yield item;
}
}
}
export function* stepHasValue(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const p = getValue(item, trackPaths);
const v = p?.value;
if (args.length === 0) {
yield item;
} else if (args[0] instanceof P) {
if (evaluatePredicate(args[0] as P, v)) yield item;
} else if (args.some(a => a == v)) {
yield item;
}
}
}
// ── Value extraction steps ────────────────────────────────────────────────────
export function* stepId(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const el = getValue(item, trackPaths);
yield wrap(el?.id, getPath(item, trackPaths), trackPaths);
}
}
export function* stepLabel(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const el = getValue(item, trackPaths);
yield wrap(el?.label, getPath(item, trackPaths), trackPaths);
}
}
export function* stepValue(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const p = getValue(item, trackPaths);
yield wrap(p?.value, getPath(item, trackPaths), trackPaths);
}
}
export function* stepKey(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
for (const item of source) {
const p = getValue(item, trackPaths);
yield wrap(propKey(p), getPath(item, trackPaths), trackPaths);
}
}
export function* stepValues(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const keys = args as string[];
for (const item of source) {
const el = getValue(item, trackPaths);
const props = allProps(el);
for (const p of props) {
if (keys.length === 0 || keys.includes(propKey(p))) {
yield wrap((p as any).value, getPath(item, trackPaths), trackPaths);
}
}
}
}
export function* stepProperties(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const keys = args as string[];
for (const item of source) {
const el = getValue(item, trackPaths);
const props = allProps(el);
for (const p of props) {
if (keys.length === 0 || keys.includes(propKey(p))) {
yield wrap(p, getPath(item, trackPaths), trackPaths);
}
}
}
}
export function* stepValueMap(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
// A boolean true as the first arg means includeTokens (valueMap(true, ...))
const includeTokens = args.length > 0 && args[0] === true;
const keys = (includeTokens ? args.slice(1) : args) as string[];
for (const item of source) {
const el = getValue(item, trackPaths);
const map = new Map<any, any[]>();
if (includeTokens) {
map.set(t.id, el?.id);
map.set(t.label, el?.label);
}
for (const p of allProps(el)) {
const k = propKey(p);
if (keys.length === 0 || keys.includes(k)) {
if (!map.has(k)) map.set(k, []);
map.get(k)!.push((p as any).value);
}
}
yield wrap(map, getPath(item, trackPaths), trackPaths);
}
}
/** Builds the stub vertex map used as Direction.OUT / Direction.IN values in edge elementMap. */
function vertexStubMap(v: Vertex): Map<any, any> {
const m = new Map<any, any>();
m.set(t.id, v.id);
m.set(t.label, v.label);
return m;
}
export function* stepElementMap(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const keys = args as string[];
for (const item of source) {
const el = getValue(item, trackPaths);
const map = new Map<any, any>();
map.set(t.id, el?.id);
map.set(t.label, el?.label);
if (el instanceof Edge) {
// Standard Gremlin edge elementMap includes Direction.OUT and Direction.IN stub maps
map.set(direction.out, vertexStubMap(el.outV));
map.set(direction.in, vertexStubMap(el.inV));
}
for (const p of allProps(el)) {
const k = propKey(p);
if (keys.length === 0 || keys.includes(k)) {
map.set(k, (p as any).value);
}
}
yield wrap(map, getPath(item, trackPaths), trackPaths);
}
}
// ── Path step ─────────────────────────────────────────────────────────────────
/**
* Sentinel signalling a non-productive by() projection (e.g. by('age') on an element
* lacking the 'age' property). Without ProductiveByStrategy — which Tiny Gremlin does not
* support — a non-productive by() filters the whole path traverser.
*/
export const NON_PRODUCTIVE = Symbol('nonProductive');
/** by(String)/by(T)/natural projection. Returns NON_PRODUCTIVE for an absent property. */
function projectByKey(el: any, key: string | null | undefined): any {
if (key == null) return el; // natural / by()
if (key === 'T.id' || key === 'id') return el?.id;
if (key === 'T.label' || key === 'label') return el?.label;
if (el instanceof Vertex || el instanceof Edge) {
const vals = getValues(el, key);
return vals.length === 0 ? NON_PRODUCTIVE : vals[0]; // first value, multi-valued
}
return el;
}
// projections carry one entry per by() modulator, applied round-robin across path
// elements. An entry is either a key (String/T/null for by(String)/by(T)/natural) or a
// single-step sub-Pipeline (by(Traversal)), run via runTraversal. Empty when no by()
// modulators are present, in which case the raw path objects are used.
export function* stepPath(
source: Iterable<StreamItem>,
projections: Arg[],
trackPaths: boolean,
runTraversal: (sub: Pipeline, object: any) => any,
): Generator<StreamItem> {
for (const item of source) {
const pathEntries: PathEntry[] = getPath(item, trackPaths);
const labels: string[][] = pathEntries.map(e => e.labels);
const objects: any[] = [];
let productive = true;
for (let idx = 0; idx < pathEntries.length; idx++) {
const object = pathEntries[idx].object;
if (projections.length === 0) { objects.push(object); continue; }
const proj = projections[idx % projections.length];
const value = Array.isArray(proj)
? runTraversal(proj as unknown as Pipeline, object)
: projectByKey(object, proj as string | null | undefined);
if (value === NON_PRODUCTIVE) { productive = false; break; }
objects.push(value);
}
if (!productive) continue; // default (non-ProductiveBy) semantics: filter the path
const pathObj = new Path(labels, objects);
// Yield as a traverser so toAsync's trackPaths unwrap produces the Path value correctly.
yield trackPaths ? { value: pathObj, path: [] } : pathObj;
}
}
// ── Range steps ───────────────────────────────────────────────────────────────
export function* stepLimit(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const n = args[0] as number;
let count = 0;
for (const item of source) {
if (count >= n) return;
yield item;
count++;
}
}
export function* stepRange(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const lo = args[0] as number;
const hi = args[1] as number;
if (lo > hi) throw new Error(`Not a legal range: [${lo}, ${hi}]`);
let i = 0;
for (const item of source) {
if (i >= hi) return;
if (i >= lo) yield item;
i++;
}
}
export function* stepSkip(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const n = args[0] as number;
let i = 0;
for (const item of source) {
if (i++ >= n) yield item;
}
}
export function* stepTail(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const n = args[0] as number;
const buf: StreamItem[] = [];
for (const item of source) buf.push(item);
yield* buf.slice(Math.max(0, buf.length - n));
}
// ── Gremlin Orderability semantics ───────────────────────────────────────────
// Total order across all types per the priority list in gremlin-semantics.asciidoc.
function gremlinTypeOrder(v: any): number {
if (v === null || v === undefined) return 1; // nulltype
if (typeof v === 'boolean') return 2; // Boolean
if (typeof v === 'number' || typeof v === 'bigint') return 3; // Number
if (v instanceof Date) return 4; // Date
if (typeof v === 'string') return 5; // String
if (v instanceof Vertex) return 7; // Vertex
if (v instanceof Edge) return 8; // Edge
if (v instanceof VertexProperty) return 9; // VertexProperty
if (v instanceof Property) return 10; // Property
if (v instanceof Path) return 11; // Path
if (v instanceof Set) return 12; // Set
if (Array.isArray(v)) return 13; // List
if (v instanceof Map) return 14; // Map
return 15; // Unknown
}
function compareNumeric(a: number, b: number): number {
// Per orderability: NaN > +Infinity > everything else
const aNaN = Number.isNaN(a), bNaN = Number.isNaN(b);
if (aNaN && bNaN) return 0;
if (aNaN) return 1;
if (bNaN) return -1;
return a < b ? -1 : a > b ? 1 : 0;
}
function compareValues(va: any, vb: any): number {
const ta = gremlinTypeOrder(va);
const tb = gremlinTypeOrder(vb);
if (ta !== tb) return ta - tb;
// Within same type family:
if (va === null || va === undefined) return 0;
if (typeof va === 'boolean') return (va ? 1 : 0) - (vb ? 1 : 0);
if (typeof va === 'number') return compareNumeric(va as number, vb as number);
if (typeof va === 'bigint') return va < vb ? -1 : va > vb ? 1 : 0;
if (va instanceof Date) return va.getTime() - (vb as Date).getTime();
if (typeof va === 'string') return va < vb ? -1 : va > vb ? 1 : 0;
// Elements: compare by id
if (va instanceof Vertex || va instanceof Edge || va instanceof VertexProperty) {
return compareValues((va as any).id, (vb as any).id);
}
if (va instanceof Property) {
const kc = compareValues((va as Property).key, (vb as Property).key);
return kc !== 0 ? kc : compareValues((va as Property).value, (vb as Property).value);
}
if (Array.isArray(va)) {
const len = Math.min(va.length, (vb as any[]).length);
for (let i = 0; i < len; i++) {
const c = compareValues(va[i], (vb as any[])[i]);
if (c !== 0) return c;
}
return va.length - (vb as any[]).length;
}
// Unknown: class name then toString()
const sa = Object.prototype.toString.call(va);
const sb = Object.prototype.toString.call(vb);
if (sa !== sb) return sa < sb ? -1 : 1;
const ts = String(va), tv = String(vb);
return ts < tv ? -1 : ts > tv ? 1 : 0;
}
// ── Gremlin Comparability semantics ──────────────────────────────────────────
// Used by P.lt/lte/gt/gte: returns null (ERROR) for cross-type or NaN comparisons.
function compareForPredicate(va: any, vb: any): number | null {
const ta = gremlinTypeOrder(va);
const tb = gremlinTypeOrder(vb);
if (ta !== tb) return null; // cross-type: ERROR
if (va === null || va === undefined) return 0; // null == null
if (typeof va === 'number') {
if (Number.isNaN(va) || Number.isNaN(vb)) return null; // NaN: ERROR
return compareNumeric(va as number, vb as number);
}
return compareValues(va, vb);
}
function getByValue(el: any, key: string | null | undefined): any {
if (key == null) return el;
if (key === 'T.id' || key === 'id') return el?.id;
if (key === 'T.label' || key === 'label') return el?.label;
if (el instanceof Vertex || el instanceof Edge) {
const vals = getValues(el, key);
return vals[0]; // first value for multi-valued properties
}
return el;
}
export function* stepOrder(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const key = args[0] as string | null | undefined;
const dir = ((args[1] as string) ?? 'asc').split('.').pop()!.toLowerCase();
const multiplier = dir === 'desc' ? -1 : 1;
const buf: StreamItem[] = [...source];
buf.sort((a, b) => {
const va = getByValue(getValue(a, trackPaths), key as string | null | undefined);
const vb = getByValue(getValue(b, trackPaths), key as string | null | undefined);
return multiplier * compareValues(va, vb);
});
yield* buf;
}
// ── Mutation steps ────────────────────────────────────────────────────────────
export function* stepAddV(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const label = (args[0] as string | undefined) ?? 'vertex';
for (const _item of source) {
const id = graph.vertices.size;
const v = new Vertex(id, label, []);
graph.vertices.set(id, v);
yield wrap(v, [], trackPaths);
}
}
export function* stepAddEWithModulators(
source: Iterable<StreamItem>,
label: string,
fromPipeline: Arg[] | null,
toPipeline: Arg[] | null,
graph: Graph,
trackPaths: boolean,
executeSubPipeline: (pipeline: Arg[], graph: Graph) => any,
): Generator<StreamItem> {
const fromVertex = fromPipeline
? executeSubPipeline(fromPipeline, graph)
: null;
const toVertex = toPipeline
? executeSubPipeline(toPipeline, graph)
: null;
if (!(fromVertex instanceof Vertex) || !(toVertex instanceof Vertex)) {
throw new Error("The addE step from/to must resolve to a Vertex or the ID of a Vertex present in the graph");
}
const id = graph.edges.size;
const edge = new Edge(id, fromVertex, label, toVertex, []);
graph.edges.set(id, edge);
graph._indexEdge(edge);
yield wrap(edge, [], trackPaths);
}
export function* stepProperty(
source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
): Generator<StreamItem> {
const key = args[0] as string;
const value = args[1];
for (const item of source) {
const el = getValue(item, trackPaths);
if (el instanceof Vertex) {
// T.label sets the element label directly; T.id sets the id
if (key === 'T.label' || key === 'label') {
(el as any).label = value;
} else if (key === 'T.id' || key === 'id') {
const oldId = el.id;
(el as any).id = value;
graph.vertices.delete(oldId);
graph.vertices.set(value, el);
} else {
const existing = el.properties as VertexProperty[];
const idx = existing.findIndex(p => p.label === key);
const vpId = idx >= 0 ? existing[idx].id : existing.length;
const vp = new VertexProperty(vpId, key, value, []);
if (idx >= 0) existing[idx] = vp;
else existing.push(vp);
}
} else if (el instanceof Edge) {
const existing = el.properties as Property[];
const idx = existing.findIndex(p => p.key === key);
const newProp = new Property(key, value);
if (idx >= 0) (existing as any)[idx] = newProp;
else existing.push(newProp);
}
yield item;
}
}
// ── Loops step ────────────────────────────────────────────────────────────────
/**
* Replaces each traverser's value with the loop count of the nearest enclosing repeat()
* (ctx.loops), typically tested by a following is(), e.g. until(__.loops().is(2)). Zero
* outside any repeat(). The loops("label") form is rejected — Tiny Gremlin has no step labels.
*/
export function* stepLoops(
source: Iterable<StreamItem>, args: Arg[], ctx: ExecutionContext,
): Generator<StreamItem> {
if (args.length > 0) {
throw new Error('loops("label") is not supported in Tiny Gremlin; step labels are excluded');
}
for (const item of source) {
yield wrap(ctx.loops, getPath(item, ctx.trackPaths), ctx.trackPaths);
}
}
// ── Repeat step ───────────────────────────────────────────────────────────────
/**
* Executes repeat() with its folded times()/until()/emit() modulators against a
* stream. The loop is driven breadth-first over the whole frontier: at each loop
* depth, every surviving traverser advances in lockstep through one shared run of
* the body. until() is the exit condition (checked before the body when declared
* first — while — or after it otherwise — do-while), times() is a loop-count exit,
* and emit() is a side output that does not stop a traverser from continuing to loop.
*
* Driving the body over the entire frontier at once (rather than per input
* traverser) is what gives barrier steps inside the body — order().by(), and the
* same for a nested repeat() — their global semantics: order() sees every traverser
* at that loop position, not just the descendants of a single start. Because the
* frontier only ever holds traversers that have survived exactly `loops` iterations,
* the loop count is uniform across the frontier and the per-traverser times()/until()
* exits remain exact.
*
* Child pipelines (the body and any until()/emit() traversal conditions) are run
* through the ExecutionContext, so a repeat() body may itself contain order().by(),
* path() projections, or a nested repeat().
*/
export function* stepRepeat(
source: Iterable<StreamItem>,
spec: RepeatSpec,
ctx: ExecutionContext,
): Generator<StreamItem> {
// until()/emit() conditions are always filter traversals here; the predicate form
// is rejected during folding (see requireTraversalCondition in LocalExecutor). Each is
// run through a context carrying the loop count it should observe via loops(), so
// until(__.loops().is(n)) sees the same counter that times(n) compares against.
const conditionHolds = (cond: Pipeline, item: StreamItem, condCtx: ExecutionContext): boolean => {
for (const _out of condCtx.runBranch(cond, [item])) return true; // filter traversal: any output => true
return false;
};
const exitBeforeBody = (item: StreamItem, loops: number, condCtx: ExecutionContext): boolean => {
if (spec.times != null && spec.timesFirst && loops >= spec.times) return true;
if (spec.untilFirst && spec.until != null && conditionHolds(spec.until, item, condCtx)) return true;
return false;
};
const exitAfterBody = (item: StreamItem, loops: number, condCtx: ExecutionContext): boolean => {
if (spec.times != null && !spec.timesFirst && loops >= spec.times) return true;
if (!spec.untilFirst && spec.until != null && conditionHolds(spec.until, item, condCtx)) return true;
return false;
};
const shouldEmit = (item: StreamItem, condCtx: ExecutionContext): boolean => {
if (!spec.emitPresent) return false;
if (spec.emit == null) return true; // bare emit() — emit every traverser
return conditionHolds(spec.emit, item, condCtx);
};
let frontier: StreamItem[] = [...source];
let loops = 0;
while (frontier.length > 0) {
// The body, the while-phase until(), and an emit-before condition all observe the
// current loop count; the do-while until() and emit-after condition observe the
// post-body count.
const curCtx = ctx.withLoops(loops);
// While-phase: traversers that hit a before-body exit leave the loop now;
// the rest (optionally emitted ahead of the body) form the body's input.
const entering: StreamItem[] = [];
for (const item of frontier) {
if (exitBeforeBody(item, loops, curCtx)) { yield item; continue; }
if (spec.emitFirst && shouldEmit(item, curCtx)) yield item;
entering.push(item);
}
if (entering.length === 0) break;
// Run the body over the entire frontier in one pass so barriers see it all.
const nextLoops = loops + 1;
const nextCtx = ctx.withLoops(nextLoops);
const nextFrontier: StreamItem[] = [];
for (const out of curCtx.runBranch(spec.body, entering)) {
if (exitAfterBody(out, nextLoops, nextCtx)) { yield out; continue; }
if (!spec.emitFirst && shouldEmit(out, nextCtx)) yield out;
nextFrontier.push(out);
}
frontier = nextFrontier;
loops = nextLoops;
}
}