@nshiab/simple-data-analysis-core 2.1.6 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/esm/class/SimpleTable.d.ts +399 -90
  2. package/esm/class/SimpleTable.d.ts.map +1 -1
  3. package/esm/class/SimpleTable.js +393 -90
  4. package/esm/helpers/buildGraphTemporalCostStateSql.d.ts +1 -1
  5. package/esm/helpers/buildGraphTemporalCostStateSql.d.ts.map +1 -1
  6. package/esm/helpers/buildGraphTemporalCostStateSql.js +23 -9
  7. package/esm/helpers/graphRouteResultSchema.d.ts +1 -1
  8. package/esm/helpers/graphRouteResultSchema.d.ts.map +1 -1
  9. package/esm/helpers/graphRouteResultSchema.js +4 -3
  10. package/esm/helpers/graphRouteResultSelect.d.ts +1 -1
  11. package/esm/helpers/graphRouteResultSelect.d.ts.map +1 -1
  12. package/esm/helpers/graphRouteResultSelect.js +5 -2
  13. package/esm/helpers/prepareGraphMetricOptions.d.ts +14 -0
  14. package/esm/helpers/prepareGraphMetricOptions.d.ts.map +1 -0
  15. package/esm/helpers/prepareGraphMetricOptions.js +41 -0
  16. package/esm/helpers/prepareGraphRouteSql.d.ts +2 -2
  17. package/esm/helpers/prepareGraphRouteSql.d.ts.map +1 -1
  18. package/esm/helpers/prepareGraphRouteSql.js +4 -4
  19. package/esm/helpers/prepareGraphTemporalSql.d.ts +3 -0
  20. package/esm/helpers/prepareGraphTemporalSql.d.ts.map +1 -1
  21. package/esm/helpers/prepareGraphTemporalSql.js +7 -0
  22. package/esm/helpers/validateGraphRouteResultSchema.d.ts +1 -1
  23. package/esm/helpers/validateGraphRouteResultSchema.d.ts.map +1 -1
  24. package/esm/helpers/validateGraphRouteResultSchema.js +3 -1
  25. package/esm/methods/distances.d.ts +2 -0
  26. package/esm/methods/distances.d.ts.map +1 -1
  27. package/esm/methods/distances.js +274 -20
  28. package/esm/methods/findCycles.d.ts +1 -0
  29. package/esm/methods/findCycles.d.ts.map +1 -1
  30. package/esm/methods/findCycles.js +22 -15
  31. package/esm/methods/paths.d.ts +1 -0
  32. package/esm/methods/paths.d.ts.map +1 -1
  33. package/esm/methods/paths.js +21 -14
  34. package/esm/methods/shortestPath.d.ts +2 -0
  35. package/esm/methods/shortestPath.d.ts.map +1 -1
  36. package/esm/methods/shortestPath.js +155 -34
  37. package/llm.md +428 -108
  38. package/llms.txt +1 -1
  39. package/package.json +1 -1
  40. package/script/class/SimpleTable.d.ts +399 -90
  41. package/script/class/SimpleTable.d.ts.map +1 -1
  42. package/script/class/SimpleTable.js +393 -90
  43. package/script/helpers/buildGraphTemporalCostStateSql.d.ts +1 -1
  44. package/script/helpers/buildGraphTemporalCostStateSql.d.ts.map +1 -1
  45. package/script/helpers/buildGraphTemporalCostStateSql.js +23 -9
  46. package/script/helpers/graphRouteResultSchema.d.ts +1 -1
  47. package/script/helpers/graphRouteResultSchema.d.ts.map +1 -1
  48. package/script/helpers/graphRouteResultSchema.js +4 -3
  49. package/script/helpers/graphRouteResultSelect.d.ts +1 -1
  50. package/script/helpers/graphRouteResultSelect.d.ts.map +1 -1
  51. package/script/helpers/graphRouteResultSelect.js +5 -2
  52. package/script/helpers/prepareGraphMetricOptions.d.ts +14 -0
  53. package/script/helpers/prepareGraphMetricOptions.d.ts.map +1 -0
  54. package/script/helpers/prepareGraphMetricOptions.js +44 -0
  55. package/script/helpers/prepareGraphRouteSql.d.ts +2 -2
  56. package/script/helpers/prepareGraphRouteSql.d.ts.map +1 -1
  57. package/script/helpers/prepareGraphRouteSql.js +4 -4
  58. package/script/helpers/prepareGraphTemporalSql.d.ts +3 -0
  59. package/script/helpers/prepareGraphTemporalSql.d.ts.map +1 -1
  60. package/script/helpers/prepareGraphTemporalSql.js +7 -0
  61. package/script/helpers/validateGraphRouteResultSchema.d.ts +1 -1
  62. package/script/helpers/validateGraphRouteResultSchema.d.ts.map +1 -1
  63. package/script/helpers/validateGraphRouteResultSchema.js +3 -1
  64. package/script/methods/distances.d.ts +2 -0
  65. package/script/methods/distances.d.ts.map +1 -1
  66. package/script/methods/distances.js +274 -20
  67. package/script/methods/findCycles.d.ts +1 -0
  68. package/script/methods/findCycles.d.ts.map +1 -1
  69. package/script/methods/findCycles.js +22 -15
  70. package/script/methods/paths.d.ts +1 -0
  71. package/script/methods/paths.d.ts.map +1 -1
  72. package/script/methods/paths.js +21 -14
  73. package/script/methods/shortestPath.d.ts +2 -0
  74. package/script/methods/shortestPath.d.ts.map +1 -1
  75. package/script/methods/shortestPath.js +155 -34
@@ -1,5 +1,6 @@
1
1
  import buildGraphTemporalCostStateSql from "../helpers/buildGraphTemporalCostStateSql.js";
2
2
  import getGraphWeightColumn from "../helpers/getGraphWeightColumn.js";
3
+ import prepareGraphMetricOptions from "../helpers/prepareGraphMetricOptions.js";
3
4
  import prepareGraphTemporalSql, { prepareGraphTemporalOptions, } from "../helpers/prepareGraphTemporalSql.js";
4
5
  import prepareGraphStarts from "../helpers/prepareGraphStarts.js";
5
6
  import prepareGraphTraversal, { validateGraphStarts, } from "../helpers/prepareGraphTraversal.js";
@@ -28,6 +29,7 @@ export default function distances(simpleTable, sourceColumn, targetColumn, start
28
29
  typeof options.outputTable !== "boolean") {
29
30
  throw new TypeError("distances() options.outputTable must be a string or boolean.");
30
31
  }
32
+ const metrics = prepareGraphMetricOptions(options, "distances()", true);
31
33
  const preparedStarts = prepareGraphStarts(startNodes, "distances()", "startNodes");
32
34
  const direction = options.direction ?? "outgoing";
33
35
  const temporalOptions = prepareGraphTemporalOptions(options, direction, "distances()");
@@ -51,13 +53,17 @@ export default function distances(simpleTable, sourceColumn, targetColumn, start
51
53
  ? preparedStarts.values
52
54
  : [temporal.gapParameter, ...preparedStarts.values];
53
55
  },
54
- buildSelect: (input, schema) => distancesSelect(input, schema, sourceColumn, targetColumn, preparedStarts, direction, options.weight, temporalOptions),
56
+ buildSelect: (input, schema) => distancesSelect(input, schema, sourceColumn, targetColumn, preparedStarts, direction, options.weight, temporalOptions, metrics),
55
57
  outputSchema: (schema) => {
56
58
  const validated = validateDistanceInputs(schema, sourceColumn, targetColumn, preparedStarts, options.weight, temporalOptions);
57
59
  return {
58
60
  start: validated.idType,
59
61
  node: validated.idType,
60
- distance: validated.distanceType,
62
+ steps: "BIGINT",
63
+ ...(options.weight === undefined
64
+ ? {}
65
+ : { total: validated.distanceType }),
66
+ ...(metrics.elapsedTime ? { elapsedTimeMs: "DOUBLE" } : {}),
61
67
  };
62
68
  },
63
69
  });
@@ -72,7 +78,7 @@ function validateDistanceInputs(schema, source, target, starts, weight, temporal
72
78
  : prepareGraphTemporalSql(schema, temporalOptions, "distances()");
73
79
  return { idType: endpoints.idType, distanceType, temporal };
74
80
  }
75
- function distancesSelect(input, schema, source, target, starts, direction, weight, temporalOptions) {
81
+ function distancesSelect(input, schema, source, target, starts, direction, weight, temporalOptions, metrics) {
76
82
  const prepared = prepareGraphTraversal(input, schema, source, target, starts, "distances()", "startNodes");
77
83
  const weightColumn = weight === undefined
78
84
  ? undefined
@@ -81,9 +87,14 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
81
87
  const edgeWeight = weightColumn === undefined
82
88
  ? `CAST(1 AS ${distanceType})`
83
89
  : `CAST(${quoteIdentifier("edges")}.${quoteIdentifier(weightColumn.column)} AS ${distanceType})`;
84
- if (temporalOptions !== undefined) {
85
- const temporal = prepareGraphTemporalSql(schema, temporalOptions, "distances()");
86
- return temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight);
90
+ const temporal = temporalOptions === undefined
91
+ ? undefined
92
+ : prepareGraphTemporalSql(schema, temporalOptions, "distances()");
93
+ if (weight !== undefined || metrics.elapsedTime) {
94
+ return summaryDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, metrics, weight !== undefined, weightColumn?.type === "FLOAT" || weightColumn?.type === "DOUBLE");
95
+ }
96
+ if (temporal !== undefined) {
97
+ return temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, "steps", false);
87
98
  }
88
99
  const relations = prepared.relationNames([
89
100
  "graph_start_values",
@@ -107,6 +118,14 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
107
118
  const edgeCost = `${quoteIdentifier("edges")}.${quoteIdentifier("__weight")}`;
108
119
  const bestDistance = `${quoteIdentifier("best")}.${quoteIdentifier("distance")}`;
109
120
  const candidateDistance = `CAST(${reachedDistance} + ${edgeCost} AS ${distanceType})`;
121
+ const candidateSteps = `${quoteIdentifier("reached")}.${quoteIdentifier("steps")} + 1`;
122
+ const bestSteps = `${quoteIdentifier("best")}.${quoteIdentifier("steps")}`;
123
+ const order = (distance, steps) => metrics.minimize === "steps"
124
+ ? `ROW(${steps}, ${distance})`
125
+ : `ROW(${distance}, ${steps})`;
126
+ const candidateOrder = order(candidateDistance, candidateSteps);
127
+ const selectedDistance = `arg_min(${candidateDistance}, ${candidateOrder})`;
128
+ const selectedSteps = `arg_min(${candidateSteps}, ${candidateOrder})`;
110
129
  return `WITH RECURSIVE ${startValuesRelation}(${quoteIdentifier("start")}) AS (
111
130
  VALUES ${prepared.startValues}
112
131
  ), ${startsRelation} AS (
@@ -120,19 +139,20 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
120
139
  ), ${distancesRelation}(
121
140
  ${quoteIdentifier("start")}, ${quoteIdentifier("node")},
122
141
  ${quoteIdentifier("__start_key")}, ${quoteIdentifier("__node_key")},
123
- ${quoteIdentifier("distance")}
142
+ ${quoteIdentifier("distance")}, ${quoteIdentifier("steps")}
124
143
  ) USING KEY(
125
144
  ${quoteIdentifier("__start_key")}, ${quoteIdentifier("__node_key")}
126
145
  ) AS (
127
146
  SELECT ${start}, ${edgeTo}, ${startKey}, ${edgeToKey},
128
- MIN(${edgeCost}) AS ${quoteIdentifier("distance")}
147
+ MIN(${edgeCost}) AS ${quoteIdentifier("distance")}, CAST(1 AS BIGINT)
129
148
  FROM ${startsRelation} AS ${quoteIdentifier("starts")}
130
149
  INNER JOIN ${edgesRelation} AS ${quoteIdentifier("edges")}
131
150
  ON ${startKey} = ${edgeFromKey}
132
151
  GROUP BY ${start}, ${edgeTo}, ${startKey}, ${edgeToKey}
133
152
  UNION
134
153
  SELECT ${reachedStart}, ${edgeTo}, ${reachedStartKey}, ${edgeToKey},
135
- MIN(${candidateDistance}) AS ${quoteIdentifier("distance")}
154
+ ${selectedDistance} AS ${quoteIdentifier("distance")},
155
+ ${selectedSteps} AS ${quoteIdentifier("steps")}
136
156
  FROM ${distancesRelation} AS ${quoteIdentifier("reached")}
137
157
  INNER JOIN ${edgesRelation} AS ${quoteIdentifier("edges")}
138
158
  ON ${reachedNodeKey} = ${edgeFromKey}
@@ -140,25 +160,259 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
140
160
  ON ${reachedStartKey} = ${quoteIdentifier("best")}.${quoteIdentifier("__start_key")}
141
161
  AND ${edgeToKey} = ${quoteIdentifier("best")}.${quoteIdentifier("__node_key")}
142
162
  GROUP BY ${reachedStart}, ${edgeTo}, ${reachedStartKey}, ${edgeToKey},
143
- ${bestDistance}
144
- HAVING ${bestDistance} IS NULL OR MIN(${candidateDistance}) < ${bestDistance}
163
+ ${bestDistance}, ${bestSteps}
164
+ HAVING ${bestDistance} IS NULL OR
165
+ ${order(selectedDistance, selectedSteps)} < ${order(bestDistance, bestSteps)}
145
166
  )
146
167
  SELECT ${quoteIdentifier("start")}, ${quoteIdentifier("node")},
147
- ${quoteIdentifier("distance")}
168
+ ${quoteIdentifier("steps")}${weight === undefined
169
+ ? ""
170
+ : `, ${quoteIdentifier("distance")} AS ${quoteIdentifier("total")}`}
148
171
  FROM ${distancesRelation}
149
- ORDER BY ${quoteIdentifier("__start_key")}, ${quoteIdentifier("distance")}, ${quoteIdentifier("__node_key")}`;
172
+ ORDER BY ${quoteIdentifier("__start_key")}, ${quoteIdentifier(metrics.minimize === "steps" ? "steps" : "distance")}, ${quoteIdentifier("__node_key")}`;
150
173
  }
151
- function temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight) {
174
+ function temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, minimize, includeTotal) {
152
175
  const q = quoteIdentifier;
153
176
  const startsSelect = `SELECT ${q("start")},
154
177
  ${prepared.key(q("start"))} AS ${q("__key")}
155
178
  FROM (VALUES ${prepared.startValues}) AS ${q("start_values")}(${q("start")})`;
156
- const costStates = buildGraphTemporalCostStateSql(prepared, startsSelect, direction, temporal, distanceType, edgeWeight);
179
+ const costStates = buildGraphTemporalCostStateSql(prepared, startsSelect, direction, temporal, distanceType, edgeWeight, [], minimize);
180
+ const order = minimize === "steps"
181
+ ? `${q("steps")}, ${q("distance")}`
182
+ : `${q("distance")}, ${q("steps")}`;
157
183
  return `${costStates.withClause}
158
- SELECT ${q("start")}, ${q("node")},
159
- MIN(${q("distance")}) AS ${q("distance")}
184
+ SELECT ${q("start")}, ${q("node")}, ${q("steps")}${includeTotal ? `, ${q("distance")} AS ${q("total")}` : ""}
160
185
  FROM ${costStates.costRelation}
161
- GROUP BY ${q("start")}, ${q("node")}, ${q("__start_key")},
162
- ${q("__node_key")}
163
- ORDER BY ${q("__start_key")}, ${q("distance")}, ${q("__node_key")}`;
186
+ QUALIFY row_number() OVER (
187
+ PARTITION BY ${q("__start_key")}, ${q("__node_key")}
188
+ ORDER BY ${order}
189
+ ) = 1
190
+ ORDER BY ${q("__start_key")}, ${q(minimize === "steps" ? "steps" : "distance")}, ${q("__node_key")}`;
191
+ }
192
+ // Distinct visited sets, metrics, and temporal boundaries are enough to extend
193
+ // summaries. No edge IDs or full connection sequences are needed. Keeping the
194
+ // visited set is necessary: equal summaries can have different legal extensions.
195
+ function summaryDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, metrics, includeTotal, floatingWeight) {
196
+ const q = quoteIdentifier;
197
+ const relations = prepared.relationNames([
198
+ "graph_starts",
199
+ "graph_temporal_settings",
200
+ "graph_event_rows",
201
+ "graph_edges",
202
+ "graph_summaries",
203
+ "graph_metric_results",
204
+ "graph_tied_summaries",
205
+ "graph_best_metrics",
206
+ "graph_metric_bounds",
207
+ ]);
208
+ const starts = relations.graph_starts;
209
+ const settings = relations.graph_temporal_settings;
210
+ const events = relations.graph_event_rows;
211
+ const edges = relations.graph_edges;
212
+ const summaries = relations.graph_summaries;
213
+ const results = relations.graph_metric_results;
214
+ const tied = relations.graph_tied_summaries;
215
+ const ref = (alias, column) => `${q(alias)}.${q(column)}`;
216
+ const searchDirection = direction;
217
+ const firstAnchor = temporal?.journeyAnchor("edges", searchDirection);
218
+ const sum = `CAST(${ref("reached", "distance")} + ${ref("edges", "__weight")} AS ${distanceType})`;
219
+ // Reported costs must not reject routes before the primary objective has
220
+ // selected its results. NULL marks overflow and propagates through sums;
221
+ // non-negative weights cannot bring an overflowing total back into range.
222
+ const nextDistance = metrics.minimize === "weight" ? sum : `TRY(${sum})`;
223
+ const temporalColumns = temporal === undefined
224
+ ? ""
225
+ : `, ${q("__event_start")}, ${q("__event_end")}`;
226
+ const eventValues = temporal === undefined
227
+ ? ""
228
+ : `, ${ref("edges", "__event_start")}, ${ref("edges", "__event_end")}`;
229
+ const metric = metrics.minimize === "steps"
230
+ ? q("steps")
231
+ : metrics.minimize === "weight"
232
+ ? q("distance")
233
+ : q("__elapsed");
234
+ // Use native time precision for selection. Collapse equal reported values
235
+ // after conversion to milliseconds, since these are summary rows.
236
+ const reportedElapsed = metrics.elapsedTime
237
+ ? `, ${temporal.elapsedMilliseconds(q("__elapsed"))} AS ${q("elapsedTimeMs")}`
238
+ : "";
239
+ const order = metrics.minimize === "steps"
240
+ ? [
241
+ q("steps"),
242
+ q("__node_key"),
243
+ ...(includeTotal ? [q("total")] : []),
244
+ ...(metrics.elapsedTime ? [q("elapsedTimeMs")] : []),
245
+ ]
246
+ : metrics.minimize === "weight"
247
+ ? [
248
+ q("total"),
249
+ q("__node_key"),
250
+ q("steps"),
251
+ ...(metrics.elapsedTime ? [q("elapsedTimeMs")] : []),
252
+ ]
253
+ : [
254
+ q("elapsedTimeMs"),
255
+ q("__node_key"),
256
+ q("steps"),
257
+ ...(includeTotal ? [q("total")] : []),
258
+ ];
259
+ const bounds = summaryBoundsSql(relations.graph_best_metrics, relations.graph_metric_bounds, starts, edges, settings, temporal, searchDirection, metrics.minimize, distanceType, floatingWeight);
260
+ return `WITH RECURSIVE ${settings} AS (
261
+ ${temporal === undefined
262
+ ? `SELECT 0 AS ${q("__gap")}`
263
+ : `SELECT CAST(? AS HUGEINT) AS ${q("__gap")}`}
264
+ ), ${events} AS MATERIALIZED (
265
+ ${prepared.edges(direction, [
266
+ `${edgeWeight} AS ${q("__weight")}`,
267
+ ...(temporal?.eventSelections("edges") ?? []),
268
+ ])}
269
+ ), ${edges} AS MATERIALIZED (
270
+ SELECT DISTINCT ${q("__from")}, ${q("__to")}, ${q("__from_key")},
271
+ ${q("__to_key")}, ${q("__weight")}${temporalColumns}
272
+ FROM ${events} AS ${q("events")}
273
+ ${temporal === undefined ? "" : `WHERE ${temporal.eventValidity("events")}`}
274
+ ), ${starts} AS (
275
+ SELECT ${q("start")}, ${prepared.key(q("start"))} AS ${q("__key")}
276
+ FROM (VALUES ${prepared.startValues}) AS ${q("start_values")}(${q("start")})
277
+ ), ${bounds.ctes}, ${summaries}(
278
+ ${q("start")}, ${q("node")}, ${q("__start_key")}, ${q("__node_key")},
279
+ ${q("__visited")}, ${q("steps")}, ${q("distance")}${temporalColumns}${metrics.elapsedTime ? `, ${q("__anchor")}` : ""}
280
+ ) AS (
281
+ SELECT ${ref("starts", "start")}, ${ref("edges", "__to")},
282
+ ${ref("starts", "__key")}, ${ref("edges", "__to_key")},
283
+ list_sort(list_distinct([${ref("starts", "__key")}, ${ref("edges", "__to_key")}])),
284
+ CAST(1 AS BIGINT), ${ref("edges", "__weight")}${eventValues}${metrics.elapsedTime ? `, ${firstAnchor}` : ""}
285
+ FROM ${starts} AS ${q("starts")}
286
+ INNER JOIN ${edges} AS ${q("edges")}
287
+ ON ${ref("starts", "__key")} = ${ref("edges", "__from_key")}
288
+ ${bounds.join(ref("starts", "__key"))}
289
+ WHERE ${bounds.test("CAST(1 AS BIGINT)", ref("edges", "__weight"), firstAnchor)}
290
+ UNION
291
+ SELECT ${ref("reached", "start")}, ${ref("edges", "__to")},
292
+ ${ref("reached", "__start_key")}, ${ref("edges", "__to_key")},
293
+ list_sort(list_distinct(list_append(${ref("reached", "__visited")}, ${ref("edges", "__to_key")}))),
294
+ ${ref("reached", "steps")} + 1, ${nextDistance}${eventValues}${metrics.elapsedTime ? `, ${ref("reached", "__anchor")}` : ""}
295
+ FROM ${summaries} AS ${q("reached")}
296
+ INNER JOIN ${edges} AS ${q("edges")}
297
+ ON ${ref("reached", "__node_key")} = ${ref("edges", "__from_key")}
298
+ CROSS JOIN ${settings} AS ${q("settings")}
299
+ ${bounds.join(ref("reached", "__start_key"))}
300
+ WHERE ${bounds.test(`${ref("reached", "steps")} + 1`, nextDistance, ref("reached", "__anchor"))}
301
+ AND ${ref("reached", "__node_key")} <> ${ref("reached", "__start_key")}
302
+ AND (NOT list_contains(${ref("reached", "__visited")}, ${ref("edges", "__to_key")})
303
+ OR ${ref("edges", "__to_key")} = ${ref("reached", "__start_key")})
304
+ ${temporal === undefined ? "" : `AND ${temporal.transition("reached", "edges", searchDirection, ref("settings", "__gap"))}`}
305
+ ), ${results} AS (
306
+ SELECT *${metrics.elapsedTime
307
+ ? `, ${temporal.journeyElapsed("summaries", ref("summaries", "__anchor"), searchDirection)} AS ${q("__elapsed")}`
308
+ : ""}
309
+ FROM ${summaries} AS ${q("summaries")}
310
+ ), ${tied} AS MATERIALIZED (
311
+ SELECT DISTINCT ${q("start")}, ${q("node")}, ${q("__start_key")}, ${q("__node_key")},
312
+ ${q("steps")}${includeTotal ? `, ${q("distance")} AS ${q("total")}` : ""}${reportedElapsed}
313
+ FROM ${results}
314
+ QUALIFY ${metric} = MIN(${metric}) OVER (
315
+ PARTITION BY ${q("__start_key")}, ${q("__node_key")}
316
+ )
317
+ )
318
+ SELECT ${q("start")}, ${q("node")}, ${q("steps")}${includeTotal
319
+ ? `, CASE WHEN ${q("total")} IS NULL
320
+ THEN error('distances() total exceeds the supported ${distanceType} range for an optimal route.')
321
+ ELSE ${q("total")} END AS ${q("total")}`
322
+ : ""}${metrics.elapsedTime ? `, ${q("elapsedTimeMs")}` : ""}
323
+ FROM ${tied}
324
+ ORDER BY ${q("__start_key")}, ${order.join(", ")}`;
325
+ }
326
+ // Compute primary optima without enumerating visited sets. Exact additive
327
+ // objectives have optimal prefixes (at the same event boundary in temporal
328
+ // traversal). Floating addition can erase prefix differences, and elapsed
329
+ // optima can depend on a later departure, so those use a per-start upper bound.
330
+ function summaryBoundsSql(best, limits, starts, edges, settings, temporal, direction, minimize, distanceType, floatingWeight) {
331
+ const q = quoteIdentifier;
332
+ const ref = (alias, column) => `${q(alias)}.${q(column)}`;
333
+ const elapsed = minimize === "elapsedTime";
334
+ const globalBound = elapsed || (minimize === "weight" && floatingWeight);
335
+ const boundaries = temporal === undefined
336
+ ? []
337
+ : ["__event_start", "__event_end"];
338
+ const keys = [
339
+ "__start_key",
340
+ "__node_key",
341
+ ...boundaries,
342
+ ...(elapsed ? ["__anchor"] : []),
343
+ ];
344
+ const firstAnchor = elapsed
345
+ ? temporal.journeyAnchor("edges", direction)
346
+ : undefined;
347
+ const firstKeys = [
348
+ ref("starts", "__key"),
349
+ ref("edges", "__to_key"),
350
+ ...boundaries.map((key) => ref("edges", key)),
351
+ ...(elapsed ? [firstAnchor] : []),
352
+ ];
353
+ const nextKeys = [
354
+ ref("reached", "__start_key"),
355
+ ref("edges", "__to_key"),
356
+ ...boundaries.map((key) => ref("edges", key)),
357
+ ...(elapsed ? [ref("reached", "__anchor")] : []),
358
+ ];
359
+ const firstMetric = minimize === "steps"
360
+ ? "CAST(1 AS BIGINT)"
361
+ : elapsed
362
+ ? temporal.journeyElapsed("edges", firstAnchor, direction)
363
+ : ref("edges", "__weight");
364
+ const nextMetric = minimize === "steps"
365
+ ? `${ref("reached", "__metric")} + 1`
366
+ : elapsed
367
+ ? temporal.journeyElapsed("edges", ref("reached", "__anchor"), direction)
368
+ : `CAST(${ref("reached", "__metric")} + ${ref("edges", "__weight")} AS ${distanceType})`;
369
+ const minimum = `MIN(${nextMetric})`;
370
+ const bestMetric = ref("best", "__metric");
371
+ const ctes = `${best}(${keys.map(q).join(", ")}, ${q("__metric")})
372
+ USING KEY(${keys.map(q).join(", ")}) AS (
373
+ SELECT ${firstKeys.join(", ")}, MIN(${firstMetric})
374
+ FROM ${starts} AS ${q("starts")}
375
+ INNER JOIN ${edges} AS ${q("edges")}
376
+ ON ${ref("starts", "__key")} = ${ref("edges", "__from_key")}
377
+ GROUP BY ${firstKeys.join(", ")}
378
+ UNION
379
+ SELECT ${nextKeys.join(", ")}, ${minimum}
380
+ FROM ${best} AS ${q("reached")}
381
+ INNER JOIN ${edges} AS ${q("edges")}
382
+ ON ${ref("reached", "__node_key")} = ${ref("edges", "__from_key")}
383
+ CROSS JOIN ${settings} AS ${q("settings")}
384
+ LEFT JOIN recurring.${best} AS ${q("best")}
385
+ ON ${keys.map((key, i) => `${ref("best", key)} = ${nextKeys[i]}`).join(" AND ")}
386
+ WHERE ${ref("reached", "__node_key")} <> ${ref("reached", "__start_key")}
387
+ ${temporal === undefined ? "" : `AND ${temporal.transition("reached", "edges", direction, ref("settings", "__gap"))}`}
388
+ GROUP BY ${nextKeys.join(", ")}, ${bestMetric}
389
+ HAVING ${bestMetric} IS NULL OR ${minimum} < ${bestMetric}
390
+ )${globalBound
391
+ ? `, ${limits} AS (
392
+ SELECT ${q("__start_key")}, MAX(${q("__minimum")}) AS ${q("__metric")}
393
+ FROM (
394
+ SELECT ${q("__start_key")}, ${q("__node_key")}, MIN(${q("__metric")}) AS ${q("__minimum")}
395
+ FROM ${best}
396
+ GROUP BY ${q("__start_key")}, ${q("__node_key")}
397
+ ) AS ${q("minima")}
398
+ GROUP BY ${q("__start_key")}
399
+ )`
400
+ : ""}`;
401
+ return {
402
+ ctes,
403
+ join: (startKey) => `INNER JOIN ${globalBound ? limits : best} AS ${q("bounds")}
404
+ ON ${ref("bounds", "__start_key")} = ${startKey}
405
+ ${globalBound
406
+ ? ""
407
+ : `AND ${ref("bounds", "__node_key")} = ${ref("edges", "__to_key")}
408
+ ${boundaries.map((key) => `AND ${ref("bounds", key)} = ${ref("edges", key)}`).join(" ")}`}`,
409
+ test: (steps, distance, anchor) => {
410
+ const metric = minimize === "steps"
411
+ ? steps
412
+ : elapsed
413
+ ? temporal.journeyElapsed("edges", anchor, direction)
414
+ : distance;
415
+ return `${metric} ${globalBound ? "<=" : "="} ${ref("bounds", "__metric")}`;
416
+ },
417
+ };
164
418
  }
@@ -3,6 +3,7 @@ import { type GraphTemporalOptions } from "../helpers/prepareGraphTemporalSql.js
3
3
  import { type GraphId } from "../helpers/prepareGraphStarts.js";
4
4
  import { type GraphDirection } from "../helpers/prepareGraphTraversal.js";
5
5
  type FindCyclesOptions = GraphTemporalOptions & {
6
+ elapsedTime?: boolean;
6
7
  direction?: GraphDirection;
7
8
  outputTable?: string | boolean;
8
9
  weight?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"findCycles.d.ts","sourceRoot":"","sources":["../../src/methods/findCycles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAMvD,OAAgC,EAC9B,KAAK,oBAAoB,EAG1B,MAAM,uCAAuC,CAAC;AAC/C,OAA2B,EACzB,KAAK,OAAO,EAEb,MAAM,kCAAkC,CAAC;AAC1C,OAA8B,EAC5B,KAAK,cAAc,EAEpB,MAAM,qCAAqC,CAAC;AAM7C,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,OAAO,GAAG,OAAO,EAAE,EAC/B,OAAO,GAAE,iBAAsB,GAC9B,WAAW,CAuHb"}
1
+ {"version":3,"file":"findCycles.d.ts","sourceRoot":"","sources":["../../src/methods/findCycles.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAMvD,OAAgC,EAC9B,KAAK,oBAAoB,EAG1B,MAAM,uCAAuC,CAAC;AAC/C,OAA2B,EACzB,KAAK,OAAO,EAEb,MAAM,kCAAkC,CAAC;AAC1C,OAA8B,EAC5B,KAAK,cAAc,EAEpB,MAAM,qCAAqC,CAAC;AAM7C,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,UAAU,CAChC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,OAAO,GAAG,OAAO,EAAE,EAC/B,OAAO,GAAE,iBAAsB,GAC9B,WAAW,CAgIb"}
@@ -1,3 +1,4 @@
1
+ import prepareGraphMetricOptions from "../helpers/prepareGraphMetricOptions.js";
1
2
  import getGraphEdgeIdColumn from "../helpers/getGraphEdgeIdColumn.js";
2
3
  import getGraphWeightColumn from "../helpers/getGraphWeightColumn.js";
3
4
  import graphRouteResultSchema from "../helpers/graphRouteResultSchema.js";
@@ -35,6 +36,7 @@ export default function findCycles(simpleTable, sourceColumn, targetColumn, edge
35
36
  typeof options.outputTable !== "boolean") {
36
37
  throw new TypeError("findCycles() options.outputTable must be a string or boolean.");
37
38
  }
39
+ const metric = prepareGraphMetricOptions(options, "findCycles()", false);
38
40
  const direction = options.direction ?? "outgoing";
39
41
  const temporalOptions = prepareGraphTemporalOptions(options, direction, "findCycles()");
40
42
  options = structuredClone(options);
@@ -53,7 +55,8 @@ export default function findCycles(simpleTable, sourceColumn, targetColumn, edge
53
55
  ? undefined
54
56
  : (input) => validateGraphTemporalEvents(input, temporalOptions, "findCycles()", parameters),
55
57
  values: (schema) => {
56
- validateFindCyclesInputs(schema, sourceColumn, targetColumn, edgeId, preparedStarts, options.weight);
58
+ const validated = validateFindCyclesInputs(schema, sourceColumn, targetColumn, edgeId, preparedStarts, options.weight);
59
+ graphRouteResultSchema(schema, validated.distanceType, "findCycles()", metric.elapsedTime);
57
60
  const temporal = temporalOptions === undefined
58
61
  ? undefined
59
62
  : prepareGraphTemporalSql(schema, temporalOptions, "findCycles()");
@@ -61,7 +64,7 @@ export default function findCycles(simpleTable, sourceColumn, targetColumn, edge
61
64
  ? preparedStarts.values
62
65
  : [temporal.gapParameter, ...preparedStarts.values];
63
66
  },
64
- buildSelect: (input, schema) => findCyclesSelect(input, schema, sourceColumn, targetColumn, edgeId, preparedStarts, direction, options.weight, temporalOptions),
67
+ buildSelect: (input, schema) => findCyclesSelect(input, schema, sourceColumn, targetColumn, edgeId, preparedStarts, direction, options.weight, temporalOptions, metric.elapsedTime),
65
68
  outputSchema: (schema) => {
66
69
  const validated = validateFindCyclesInputs(schema, sourceColumn, targetColumn, edgeId, preparedStarts, options.weight);
67
70
  if (temporalOptions !== undefined) {
@@ -69,7 +72,7 @@ export default function findCycles(simpleTable, sourceColumn, targetColumn, edge
69
72
  }
70
73
  return {
71
74
  start: validated.nodeIdType,
72
- ...graphRouteResultSchema(schema, validated.distanceType, "findCycles()"),
75
+ ...graphRouteResultSchema(schema, validated.distanceType, "findCycles()", metric.elapsedTime),
73
76
  };
74
77
  },
75
78
  });
@@ -87,7 +90,7 @@ function validateFindCyclesInputs(schema, source, target, edgeId, starts, weight
87
90
  nodeIdType: endpoints.idType,
88
91
  };
89
92
  }
90
- function findCyclesSelect(input, schema, source, target, edgeId, starts, direction, weight, temporalOptions) {
93
+ function findCyclesSelect(input, schema, source, target, edgeId, starts, direction, weight, temporalOptions, elapsedTime) {
91
94
  const prepared = prepareGraphTraversal(input, schema, source, target, starts, "findCycles()", "startNodes");
92
95
  const edgeIdColumn = getGraphEdgeIdColumn(schema, edgeId, "findCycles()");
93
96
  const weightColumn = weight === undefined
@@ -103,10 +106,10 @@ function findCyclesSelect(input, schema, source, target, edgeId, starts, directi
103
106
  const edgeWeight = weightColumn === undefined
104
107
  ? `CAST(1 AS ${distanceType})`
105
108
  : `CAST(${q("edges")}.${q(weightColumn.column)} AS ${distanceType})`;
106
- const stepType = `STRUCT(${q("edgeId")} ${edgeIdColumn.idType}, ${q("source")} ${prepared.endpoints.idType}, ${q("target")} ${prepared.endpoints.idType}, ${q("weight")} ${distanceType}, ${q("distance")} ${distanceType})`;
109
+ const stepType = `STRUCT(${q("edgeId")} ${edgeIdColumn.idType}, ${q("source")} ${prepared.endpoints.idType}, ${q("target")} ${prepared.endpoints.idType}, ${q("weight")} ${distanceType}, ${q("distance")} ${distanceType}${elapsedTime ? `, ${q("elapsedTimeMs")} DOUBLE` : ""})`;
107
110
  if (temporalOptions !== undefined) {
108
111
  const temporal = prepareGraphTemporalSql(schema, temporalOptions, "findCycles()");
109
- return temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, edgeKey, edgeWeight, distanceType, stepType, input, edgeIdColumn);
112
+ return temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, edgeKey, edgeWeight, distanceType, stepType, input, edgeIdColumn, elapsedTime);
110
113
  }
111
114
  const relations = prepared.relationNames([
112
115
  "graph_start_values",
@@ -210,7 +213,7 @@ function findCyclesSelect(input, schema, source, target, edgeId, starts, directi
210
213
  )
211
214
  ${graphRouteResultSelect(rankedRelation, input, edgeIdColumn, true)}`;
212
215
  }
213
- function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, edgeKey, edgeWeight, distanceType, stepType, input, edgeIdColumn) {
216
+ function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, edgeKey, edgeWeight, distanceType, stepType, input, edgeIdColumn, elapsedTime) {
214
217
  const q = quoteIdentifier;
215
218
  const relations = prepared.relationNames([
216
219
  "graph_start_values",
@@ -240,12 +243,16 @@ function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, ed
240
243
  const walkDistance = `${q("walks")}.${q("distance")}`;
241
244
  const candidateDistance = `CAST(${walkDistance} + ${edgeCost} AS ${distanceType})`;
242
245
  const gap = `${q("settings")}.${q("__gap")}`;
243
- const step = (distance) => `struct_pack(
246
+ const firstAnchor = temporal.journeyAnchor("edges", direction);
247
+ const anchor = `${q("walks")}.${q("__journey_anchor")}`;
248
+ const step = (distance, journeyAnchor) => `struct_pack(
244
249
  ${q("edgeId")} := ${edgeIdentity},
245
250
  ${q("source")} := ${edgeFrom},
246
251
  ${q("target")} := ${edgeTo},
247
252
  ${q("weight")} := ${edgeCost},
248
- ${q("distance")} := ${distance}
253
+ ${q("distance")} := ${distance}${elapsedTime
254
+ ? `, ${q("elapsedTimeMs")} := ${temporal.elapsedMilliseconds(temporal.journeyElapsed("edges", journeyAnchor, direction))}`
255
+ : ""}
249
256
  )`;
250
257
  return `WITH RECURSIVE ${settingsRelation} AS MATERIALIZED (
251
258
  SELECT CAST(? AS HUGEINT) AS ${q("__gap")}
@@ -271,7 +278,7 @@ function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, ed
271
278
  ${q("__node_key")}, ${q("__visited")}, ${q("__event_ids")},
272
279
  ${q("__edge_keys")}, ${q("steps")}, ${q("distance")},
273
280
  ${q("closed")}, ${q("__event_id")}, ${q("__event_start")},
274
- ${q("__event_end")}
281
+ ${q("__event_end")}${elapsedTime ? `, ${q("__journey_anchor")}` : ""}
275
282
  ) AS (
276
283
  SELECT ${q("starts")}.${q("start")},
277
284
  ${q("starts")}.${q("__start_key")}, ${edgeTo}, ${edgeToKey},
@@ -280,11 +287,11 @@ function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, ed
280
287
  ELSE [${edgeFromKey}, ${edgeToKey}]
281
288
  END,
282
289
  [${q("edges")}.${q("__event_id")}], [${edgeIdentityKey}],
283
- [${step(edgeCost)}]::${stepType}[], ${edgeCost},
290
+ [${step(edgeCost, firstAnchor)}]::${stepType}[], ${edgeCost},
284
291
  ${edgeFromKey} = ${edgeToKey},
285
292
  ${q("edges")}.${q("__event_id")},
286
293
  ${q("edges")}.${q("__event_start")},
287
- ${q("edges")}.${q("__event_end")}
294
+ ${q("edges")}.${q("__event_end")}${elapsedTime ? `, ${firstAnchor}` : ""}
288
295
  FROM ${edgesRelation} AS ${q("edges")}
289
296
  INNER JOIN ${startsRelation} AS ${q("starts")}
290
297
  ON ${q("starts")}.${q("__start_key")} = ${edgeFromKey}
@@ -295,12 +302,12 @@ function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, ed
295
302
  list_append(${q("walks")}.${q("__event_ids")},
296
303
  ${q("edges")}.${q("__event_id")}),
297
304
  list_append(${q("walks")}.${q("__edge_keys")}, ${edgeIdentityKey}),
298
- list_append(${q("walks")}.${q("steps")}, ${step(candidateDistance)}),
305
+ list_append(${q("walks")}.${q("steps")}, ${step(candidateDistance, anchor)}),
299
306
  ${candidateDistance},
300
307
  ${edgeToKey} = ${q("walks")}.${q("__start_key")},
301
308
  ${q("edges")}.${q("__event_id")},
302
309
  ${q("edges")}.${q("__event_start")},
303
- ${q("edges")}.${q("__event_end")}
310
+ ${q("edges")}.${q("__event_end")}${elapsedTime ? `, ${anchor}` : ""}
304
311
  FROM ${walksRelation} AS ${q("walks")}
305
312
  INNER JOIN ${edgesRelation} AS ${q("edges")}
306
313
  ON ${q("walks")}.${q("__node_key")} = ${edgeFromKey}
@@ -326,5 +333,5 @@ function temporalFindCyclesSelect(prepared, direction, temporal, typedEdgeId, ed
326
333
  AS BIGINT) AS ${q("pathId")}, ${q("steps")}
327
334
  FROM ${completeRelation}
328
335
  )
329
- ${graphRouteResultSelect(rankedRelation, input, edgeIdColumn, true)}`;
336
+ ${graphRouteResultSelect(rankedRelation, input, edgeIdColumn, true, elapsedTime)}`;
330
337
  }
@@ -3,6 +3,7 @@ import type { GraphId } from "../helpers/prepareGraphStarts.js";
3
3
  import { type GraphTemporalOptions } from "../helpers/prepareGraphTemporalSql.js";
4
4
  import type { GraphDirection } from "../helpers/prepareGraphTraversal.js";
5
5
  type PathsOptions = GraphTemporalOptions & {
6
+ elapsedTime?: boolean;
6
7
  direction?: GraphDirection;
7
8
  outputTable?: string | boolean;
8
9
  weight?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/methods/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AASvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAgC,EAC9B,KAAK,oBAAoB,EAG1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAM1E,KAAK,YAAY,GAAG,oBAAoB,GAAG;IACzC,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,KAAK,CAC3B,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,YAAiB,GACzB,WAAW,CA+Gb"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/methods/paths.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AASvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAgC,EAC9B,KAAK,oBAAoB,EAG1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAM1E,KAAK,YAAY,GAAG,oBAAoB,GAAG;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,KAAK,CAC3B,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,YAAiB,GACzB,WAAW,CA4Hb"}