@nshiab/simple-data-analysis-core 2.1.7 → 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 (57) hide show
  1. package/esm/class/SimpleTable.d.ts +187 -104
  2. package/esm/class/SimpleTable.d.ts.map +1 -1
  3. package/esm/class/SimpleTable.js +185 -102
  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 +3 -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 +2 -2
  13. package/esm/helpers/prepareGraphMetricOptions.d.ts +2 -2
  14. package/esm/helpers/prepareGraphMetricOptions.d.ts.map +1 -1
  15. package/esm/helpers/prepareGraphMetricOptions.js +5 -4
  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/validateGraphRouteResultSchema.d.ts +1 -1
  20. package/esm/helpers/validateGraphRouteResultSchema.d.ts.map +1 -1
  21. package/esm/helpers/validateGraphRouteResultSchema.js +3 -1
  22. package/esm/methods/distances.d.ts +1 -1
  23. package/esm/methods/distances.d.ts.map +1 -1
  24. package/esm/methods/distances.js +235 -73
  25. package/esm/methods/shortestPath.d.ts +1 -1
  26. package/esm/methods/shortestPath.d.ts.map +1 -1
  27. package/esm/methods/shortestPath.js +44 -28
  28. package/llm.md +207 -112
  29. package/llms.txt +1 -1
  30. package/package.json +1 -1
  31. package/script/class/SimpleTable.d.ts +187 -104
  32. package/script/class/SimpleTable.d.ts.map +1 -1
  33. package/script/class/SimpleTable.js +185 -102
  34. package/script/helpers/buildGraphTemporalCostStateSql.d.ts +1 -1
  35. package/script/helpers/buildGraphTemporalCostStateSql.d.ts.map +1 -1
  36. package/script/helpers/buildGraphTemporalCostStateSql.js +23 -9
  37. package/script/helpers/graphRouteResultSchema.d.ts +1 -1
  38. package/script/helpers/graphRouteResultSchema.d.ts.map +1 -1
  39. package/script/helpers/graphRouteResultSchema.js +3 -3
  40. package/script/helpers/graphRouteResultSelect.d.ts +1 -1
  41. package/script/helpers/graphRouteResultSelect.d.ts.map +1 -1
  42. package/script/helpers/graphRouteResultSelect.js +2 -2
  43. package/script/helpers/prepareGraphMetricOptions.d.ts +2 -2
  44. package/script/helpers/prepareGraphMetricOptions.d.ts.map +1 -1
  45. package/script/helpers/prepareGraphMetricOptions.js +5 -4
  46. package/script/helpers/prepareGraphRouteSql.d.ts +2 -2
  47. package/script/helpers/prepareGraphRouteSql.d.ts.map +1 -1
  48. package/script/helpers/prepareGraphRouteSql.js +4 -4
  49. package/script/helpers/validateGraphRouteResultSchema.d.ts +1 -1
  50. package/script/helpers/validateGraphRouteResultSchema.d.ts.map +1 -1
  51. package/script/helpers/validateGraphRouteResultSchema.js +3 -1
  52. package/script/methods/distances.d.ts +1 -1
  53. package/script/methods/distances.d.ts.map +1 -1
  54. package/script/methods/distances.js +235 -73
  55. package/script/methods/shortestPath.d.ts +1 -1
  56. package/script/methods/shortestPath.d.ts.map +1 -1
  57. package/script/methods/shortestPath.js +44 -28
@@ -59,7 +59,10 @@ export default function distances(simpleTable, sourceColumn, targetColumn, start
59
59
  return {
60
60
  start: validated.idType,
61
61
  node: validated.idType,
62
- total: validated.distanceType,
62
+ steps: "BIGINT",
63
+ ...(options.weight === undefined
64
+ ? {}
65
+ : { total: validated.distanceType }),
63
66
  ...(metrics.elapsedTime ? { elapsedTimeMs: "DOUBLE" } : {}),
64
67
  };
65
68
  },
@@ -84,12 +87,14 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
84
87
  const edgeWeight = weightColumn === undefined
85
88
  ? `CAST(1 AS ${distanceType})`
86
89
  : `CAST(${quoteIdentifier("edges")}.${quoteIdentifier(weightColumn.column)} AS ${distanceType})`;
87
- if (temporalOptions !== undefined) {
88
- const temporal = prepareGraphTemporalSql(schema, temporalOptions, "distances()");
89
- if (metrics.elapsedTime) {
90
- return elapsedDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, metrics.minimize);
91
- }
92
- 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);
93
98
  }
94
99
  const relations = prepared.relationNames([
95
100
  "graph_start_values",
@@ -113,6 +118,14 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
113
118
  const edgeCost = `${quoteIdentifier("edges")}.${quoteIdentifier("__weight")}`;
114
119
  const bestDistance = `${quoteIdentifier("best")}.${quoteIdentifier("distance")}`;
115
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})`;
116
129
  return `WITH RECURSIVE ${startValuesRelation}(${quoteIdentifier("start")}) AS (
117
130
  VALUES ${prepared.startValues}
118
131
  ), ${startsRelation} AS (
@@ -126,19 +139,20 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
126
139
  ), ${distancesRelation}(
127
140
  ${quoteIdentifier("start")}, ${quoteIdentifier("node")},
128
141
  ${quoteIdentifier("__start_key")}, ${quoteIdentifier("__node_key")},
129
- ${quoteIdentifier("distance")}
142
+ ${quoteIdentifier("distance")}, ${quoteIdentifier("steps")}
130
143
  ) USING KEY(
131
144
  ${quoteIdentifier("__start_key")}, ${quoteIdentifier("__node_key")}
132
145
  ) AS (
133
146
  SELECT ${start}, ${edgeTo}, ${startKey}, ${edgeToKey},
134
- MIN(${edgeCost}) AS ${quoteIdentifier("distance")}
147
+ MIN(${edgeCost}) AS ${quoteIdentifier("distance")}, CAST(1 AS BIGINT)
135
148
  FROM ${startsRelation} AS ${quoteIdentifier("starts")}
136
149
  INNER JOIN ${edgesRelation} AS ${quoteIdentifier("edges")}
137
150
  ON ${startKey} = ${edgeFromKey}
138
151
  GROUP BY ${start}, ${edgeTo}, ${startKey}, ${edgeToKey}
139
152
  UNION
140
153
  SELECT ${reachedStart}, ${edgeTo}, ${reachedStartKey}, ${edgeToKey},
141
- MIN(${candidateDistance}) AS ${quoteIdentifier("distance")}
154
+ ${selectedDistance} AS ${quoteIdentifier("distance")},
155
+ ${selectedSteps} AS ${quoteIdentifier("steps")}
142
156
  FROM ${distancesRelation} AS ${quoteIdentifier("reached")}
143
157
  INNER JOIN ${edgesRelation} AS ${quoteIdentifier("edges")}
144
158
  ON ${reachedNodeKey} = ${edgeFromKey}
@@ -146,111 +160,259 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
146
160
  ON ${reachedStartKey} = ${quoteIdentifier("best")}.${quoteIdentifier("__start_key")}
147
161
  AND ${edgeToKey} = ${quoteIdentifier("best")}.${quoteIdentifier("__node_key")}
148
162
  GROUP BY ${reachedStart}, ${edgeTo}, ${reachedStartKey}, ${edgeToKey},
149
- ${bestDistance}
150
- HAVING ${bestDistance} IS NULL OR MIN(${candidateDistance}) < ${bestDistance}
163
+ ${bestDistance}, ${bestSteps}
164
+ HAVING ${bestDistance} IS NULL OR
165
+ ${order(selectedDistance, selectedSteps)} < ${order(bestDistance, bestSteps)}
151
166
  )
152
167
  SELECT ${quoteIdentifier("start")}, ${quoteIdentifier("node")},
153
- ${quoteIdentifier("distance")} AS ${quoteIdentifier("total")}
168
+ ${quoteIdentifier("steps")}${weight === undefined
169
+ ? ""
170
+ : `, ${quoteIdentifier("distance")} AS ${quoteIdentifier("total")}`}
154
171
  FROM ${distancesRelation}
155
- 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")}`;
156
173
  }
157
- function temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight) {
174
+ function temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, minimize, includeTotal) {
158
175
  const q = quoteIdentifier;
159
176
  const startsSelect = `SELECT ${q("start")},
160
177
  ${prepared.key(q("start"))} AS ${q("__key")}
161
178
  FROM (VALUES ${prepared.startValues}) AS ${q("start_values")}(${q("start")})`;
162
- 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")}`;
163
183
  return `${costStates.withClause}
164
- SELECT ${q("start")}, ${q("node")},
165
- MIN(${q("distance")}) AS ${q("total")}
184
+ SELECT ${q("start")}, ${q("node")}, ${q("steps")}${includeTotal ? `, ${q("distance")} AS ${q("total")}` : ""}
166
185
  FROM ${costStates.costRelation}
167
- GROUP BY ${q("start")}, ${q("node")}, ${q("__start_key")},
168
- ${q("__node_key")}
169
- ORDER BY ${q("__start_key")}, ${q("total")}, ${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")}`;
170
191
  }
171
- function elapsedDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, minimize) {
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) {
172
196
  const q = quoteIdentifier;
173
197
  const relations = prepared.relationNames([
174
198
  "graph_starts",
175
199
  "graph_temporal_settings",
176
200
  "graph_event_rows",
177
201
  "graph_edges",
178
- "graph_elapsed_states",
179
- "graph_elapsed_results",
202
+ "graph_summaries",
203
+ "graph_metric_results",
204
+ "graph_tied_summaries",
205
+ "graph_best_metrics",
206
+ "graph_metric_bounds",
180
207
  ]);
181
208
  const starts = relations.graph_starts;
182
209
  const settings = relations.graph_temporal_settings;
183
210
  const events = relations.graph_event_rows;
184
211
  const edges = relations.graph_edges;
185
- const states = relations.graph_elapsed_states;
186
- const results = relations.graph_elapsed_results;
212
+ const summaries = relations.graph_summaries;
213
+ const results = relations.graph_metric_results;
214
+ const tied = relations.graph_tied_summaries;
187
215
  const ref = (alias, column) => `${q(alias)}.${q(column)}`;
188
- const anchor = temporal.journeyAnchor("edges", direction);
189
- const reachedAnchor = ref("reached", "__anchor");
190
- const candidateDistance = `CAST(${ref("reached", "distance")} + ${ref("edges", "__weight")} AS ${distanceType})`;
191
- const order = minimize === "elapsedTime"
192
- ? `${q("__elapsed")}, ${q("distance")}`
193
- : `${q("distance")}, ${q("__elapsed")}`;
194
- // A physical event and the first departure (last arrival for incoming
195
- // traversal) fix elapsed time. Keep the cheapest cost for each such state;
196
- // merging different anchors would lose later-departing, faster journeys.
197
- // The state space is finite, and strict improvements terminate zero cycles.
198
- return `WITH RECURSIVE ${settings} AS MATERIALIZED (
199
- SELECT CAST(? AS HUGEINT) AS ${q("__gap")}
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")}`}
200
264
  ), ${events} AS MATERIALIZED (
201
265
  ${prepared.edges(direction, [
202
266
  `${edgeWeight} AS ${q("__weight")}`,
203
- ...temporal.eventSelections("edges"),
267
+ ...(temporal?.eventSelections("edges") ?? []),
204
268
  ])}
205
269
  ), ${edges} AS MATERIALIZED (
206
- SELECT * FROM ${events} AS ${q("events")}
207
- WHERE ${temporal.eventValidity("events")}
208
- ), ${starts} 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 (
209
275
  SELECT ${q("start")}, ${prepared.key(q("start"))} AS ${q("__key")}
210
276
  FROM (VALUES ${prepared.startValues}) AS ${q("start_values")}(${q("start")})
211
- ), ${states}(
277
+ ), ${bounds.ctes}, ${summaries}(
212
278
  ${q("start")}, ${q("node")}, ${q("__start_key")}, ${q("__node_key")},
213
- ${q("__event_id")}, ${q("__event_start")}, ${q("__event_end")},
214
- ${q("__anchor")}, ${q("distance")}
215
- ) USING KEY(${q("__start_key")}, ${q("__event_id")}, ${q("__anchor")}) AS (
279
+ ${q("__visited")}, ${q("steps")}, ${q("distance")}${temporalColumns}${metrics.elapsedTime ? `, ${q("__anchor")}` : ""}
280
+ ) AS (
216
281
  SELECT ${ref("starts", "start")}, ${ref("edges", "__to")},
217
282
  ${ref("starts", "__key")}, ${ref("edges", "__to_key")},
218
- ${ref("edges", "__event_id")}, ${ref("edges", "__event_start")},
219
- ${ref("edges", "__event_end")}, ${anchor}, ${ref("edges", "__weight")}
283
+ list_sort(list_distinct([${ref("starts", "__key")}, ${ref("edges", "__to_key")}])),
284
+ CAST(1 AS BIGINT), ${ref("edges", "__weight")}${eventValues}${metrics.elapsedTime ? `, ${firstAnchor}` : ""}
220
285
  FROM ${starts} AS ${q("starts")}
221
286
  INNER JOIN ${edges} AS ${q("edges")}
222
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)}
223
290
  UNION
224
291
  SELECT ${ref("reached", "start")}, ${ref("edges", "__to")},
225
292
  ${ref("reached", "__start_key")}, ${ref("edges", "__to_key")},
226
- ${ref("edges", "__event_id")}, ${ref("edges", "__event_start")},
227
- ${ref("edges", "__event_end")}, ${reachedAnchor},
228
- MIN(${candidateDistance}) AS ${q("distance")}
229
- FROM ${states} AS ${q("reached")}
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")}
230
296
  INNER JOIN ${edges} AS ${q("edges")}
231
297
  ON ${ref("reached", "__node_key")} = ${ref("edges", "__from_key")}
232
298
  CROSS JOIN ${settings} AS ${q("settings")}
233
- LEFT JOIN recurring.${states} AS ${q("best")}
234
- ON ${ref("reached", "__start_key")} = ${ref("best", "__start_key")}
235
- AND ${ref("edges", "__event_id")} = ${ref("best", "__event_id")}
236
- AND ${reachedAnchor} = ${ref("best", "__anchor")}
237
- WHERE ${temporal.transition("reached", "edges", direction, ref("settings", "__gap"))}
238
- GROUP BY ${ref("reached", "start")}, ${ref("edges", "__to")},
239
- ${ref("reached", "__start_key")}, ${ref("edges", "__to_key")},
240
- ${ref("edges", "__event_id")}, ${ref("edges", "__event_start")},
241
- ${ref("edges", "__event_end")}, ${reachedAnchor}, ${ref("best", "distance")}
242
- HAVING ${ref("best", "distance")} IS NULL OR
243
- MIN(${candidateDistance}) < ${ref("best", "distance")}
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"))}`}
244
305
  ), ${results} AS (
245
- SELECT *, ${temporal.journeyElapsed("states", ref("states", "__anchor"), direction)} AS ${q("__elapsed")}
246
- FROM ${states} AS ${q("states")}
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
+ )
247
317
  )
248
- SELECT ${q("start")}, ${q("node")}, ${q("distance")} AS ${q("total")},
249
- ${temporal.elapsedMilliseconds(q("__elapsed"))} AS ${q("elapsedTimeMs")}
250
- FROM ${results}
251
- QUALIFY row_number() OVER (
252
- PARTITION BY ${q("__start_key")}, ${q("__node_key")}
253
- ORDER BY ${order}, ${q("__event_id")}, ${q("__anchor")}
254
- ) = 1
255
- ORDER BY ${q("__start_key")}, ${order}, ${q("__node_key")}`;
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
+ };
256
418
  }
@@ -7,7 +7,7 @@ type ShortestPathOptions = GraphTemporalOptions & {
7
7
  outputTable?: string | boolean;
8
8
  weight?: string;
9
9
  elapsedTime?: boolean;
10
- minimize?: "weight" | "elapsedTime";
10
+ minimize?: "steps" | "weight" | "elapsedTime";
11
11
  };
12
12
  export default function shortestPath(simpleTable: SimpleTable, sourceColumn: string, targetColumn: string, edgeId: string, start: GraphId, end: GraphId, options?: ShortestPathOptions): SimpleTable;
13
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"shortestPath.d.ts","sourceRoot":"","sources":["../../src/methods/shortestPath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAMvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAShE,OAAgC,EAC9B,KAAK,oBAAoB,EAG1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAK1E,KAAK,mBAAmB,GAAG,oBAAoB,GAAG;IAChD,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;CACrC,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,YAAY,CAClC,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,mBAAwB,GAChC,WAAW,CAsHb"}
1
+ {"version":3,"file":"shortestPath.d.ts","sourceRoot":"","sources":["../../src/methods/shortestPath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAMvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAShE,OAAgC,EAC9B,KAAK,oBAAoB,EAG1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAK1E,KAAK,mBAAmB,GAAG,oBAAoB,GAAG;IAChD,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAC;CAC/C,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,YAAY,CAClC,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,mBAAwB,GAChC,WAAW,CAyHb"}
@@ -53,7 +53,7 @@ export default function shortestPath(simpleTable, sourceColumn, targetColumn, ed
53
53
  ? undefined
54
54
  : (input) => validateGraphTemporalEvents(input, temporalOptions, "shortestPath()", parameters),
55
55
  values: (schema) => {
56
- validateGraphRouteInputs(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()");
56
+ validateGraphRouteInputs(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()", options.weight !== undefined);
57
57
  const temporal = temporalOptions === undefined
58
58
  ? undefined
59
59
  : prepareGraphTemporalSql(schema, temporalOptions, "shortestPath()");
@@ -63,27 +63,28 @@ export default function shortestPath(simpleTable, sourceColumn, targetColumn, ed
63
63
  },
64
64
  buildSelect: (input, schema) => shortestPathSelect(input, schema, sourceColumn, targetColumn, edgeId, endpoints, direction, options.weight, temporalOptions, metrics),
65
65
  outputSchema: (schema) => {
66
- const validated = validateGraphRouteInputs(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()");
66
+ const validated = validateGraphRouteInputs(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()", options.weight !== undefined);
67
67
  if (temporalOptions !== undefined) {
68
68
  prepareGraphTemporalSql(schema, temporalOptions, "shortestPath()");
69
69
  }
70
- return graphRouteResultSchema(schema, validated.distanceType, "shortestPath()", metrics.elapsedTime);
70
+ return graphRouteResultSchema(schema, validated.distanceType, "shortestPath()", metrics.elapsedTime, options.weight !== undefined);
71
71
  },
72
72
  });
73
73
  }
74
74
  function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoints, direction, weight, temporalOptions, metrics) {
75
- const route = prepareGraphRouteSql(input, schema, source, target, edgeId, routeEndpoints, weight, "shortestPath()");
75
+ const route = prepareGraphRouteSql(input, schema, source, target, edgeId, routeEndpoints, weight, "shortestPath()", weight !== undefined);
76
76
  const prepared = route.traversal;
77
77
  if (temporalOptions !== undefined) {
78
78
  const temporal = prepareGraphTemporalSql(schema, temporalOptions, "shortestPath()");
79
- return temporalShortestPathSelect(route, direction, temporal, metrics);
79
+ return temporalShortestPathSelect(route, direction, temporal, metrics, weight !== undefined);
80
80
  }
81
81
  const distanceType = route.distanceType;
82
82
  // Exact shortest routes have optimal prefixes. Floating addition can erase
83
83
  // a prefix-cost difference later, so retain all simple prefixes up to the
84
84
  // best complete cost before selecting the full-route ties.
85
- const floating = route.weightType === "FLOAT" ||
86
- route.weightType === "DOUBLE";
85
+ const floating = metrics.minimize === "weight" &&
86
+ (route.weightType === "FLOAT" || route.weightType === "DOUBLE");
87
+ const metricType = metrics.minimize === "steps" ? "BIGINT" : distanceType;
87
88
  const relations = prepared.relationNames([
88
89
  "graph_route_endpoints",
89
90
  "graph_edges",
@@ -109,8 +110,17 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
109
110
  const edgeToKey = `${q("edges")}.${q("__to_key")}`;
110
111
  const edgeTo = `${q("edges")}.${q("__to")}`;
111
112
  const edgeCost = `${q("edges")}.${q("__weight")}`;
113
+ const edgeMetric = metrics.minimize === "steps"
114
+ ? "CAST(1 AS BIGINT)"
115
+ : edgeCost;
112
116
  const routeDistance = `${q("routes")}.${q("distance")}`;
113
117
  const candidateDistance = `CAST(${routeDistance} + ${edgeCost} AS ${distanceType})`;
118
+ const routeMetric = metrics.minimize === "steps"
119
+ ? `len(${q("routes")}.${q("steps")})`
120
+ : routeDistance;
121
+ const candidateMetric = metrics.minimize === "steps"
122
+ ? `${routeMetric} + 1`
123
+ : candidateDistance;
114
124
  return `WITH RECURSIVE ${endpointRelation}(
115
125
  ${q("start")}, ${q("end")}, ${q("__start_key")}, ${q("__end_key")}
116
126
  ) AS (
@@ -142,13 +152,13 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
142
152
  ${q("node")}, ${q("__node_key")}, ${q("distance")}
143
153
  ) USING KEY(${q("__node_key")}) AS (
144
154
  SELECT ${q("start")}, ${q("__start_key")},
145
- CAST(0 AS ${distanceType})
155
+ CAST(0 AS ${metricType})
146
156
  FROM ${endpointRelation}
147
157
  INNER JOIN ${toEndRelation}
148
158
  ON ${q("__start_key")} = ${q("__key")}
149
159
  UNION
150
160
  SELECT ${edgeTo}, ${edgeToKey},
151
- MIN(CAST(${q("reached")}.${q("distance")} + ${edgeCost} AS ${distanceType}))
161
+ MIN(CAST(${q("reached")}.${q("distance")} + ${edgeMetric} AS ${metricType}))
152
162
  FROM ${distancesRelation} AS ${q("reached")}
153
163
  INNER JOIN ${edgesRelation} AS ${q("edges")}
154
164
  ON ${q("reached")}.${q("__node_key")} = ${edgeFromKey}
@@ -158,7 +168,7 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
158
168
  ON ${edgeToKey} = ${q("best")}.${q("__node_key")}
159
169
  GROUP BY ${edgeTo}, ${edgeToKey}, ${q("best")}.${q("distance")}
160
170
  HAVING ${q("best")}.${q("distance")} IS NULL
161
- OR MIN(CAST(${q("reached")}.${q("distance")} + ${edgeCost} AS ${distanceType})) < ${q("best")}.${q("distance")}
171
+ OR MIN(CAST(${q("reached")}.${q("distance")} + ${edgeMetric} AS ${metricType})) < ${q("best")}.${q("distance")}
162
172
  ), ${bestTotalRelation} AS (
163
173
  SELECT ${q("distances")}.${q("distance")}
164
174
  FROM ${distancesRelation} AS ${q("distances")}
@@ -197,23 +207,23 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
197
207
  WHERE ${q("routes")}.${q("__node_key")} <> ${q("__end_key")}
198
208
  AND NOT list_contains(${q("routes")}.${q("__visited")}, ${edgeToKey})
199
209
  AND ${floating
200
- ? `${candidateDistance} <= ${q("best_total")}.${q("distance")}`
201
- : `${candidateDistance} = ${q("best_prefix")}.${q("distance")}`}
210
+ ? `${candidateMetric} <= ${q("best_total")}.${q("distance")}`
211
+ : `${candidateMetric} = ${q("best_prefix")}.${q("distance")}`}
202
212
  ), ${shortestRelation} AS (
203
213
  SELECT ${q("__edge_keys")}, ${q("steps")}
204
- FROM ${routesRelation}
214
+ FROM ${routesRelation} AS ${q("routes")}
205
215
  CROSS JOIN ${endpointRelation}
206
216
  CROSS JOIN ${bestTotalRelation} AS ${q("best_total")}
207
217
  WHERE ${q("__node_key")} = ${q("__end_key")}
208
- AND ${routesRelation}.${q("distance")} = ${q("best_total")}.${q("distance")}
218
+ AND ${routeMetric} = ${q("best_total")}.${q("distance")}
209
219
  ), ${rankedRelation} AS (
210
220
  SELECT CAST(row_number() OVER (ORDER BY ${q("__edge_keys")}) - 1 AS BIGINT) AS ${q("pathId")},
211
221
  ${q("steps")}
212
222
  FROM ${shortestRelation}
213
223
  )
214
- ${graphRouteResultSelect(rankedRelation, route.input, route.edgeIdColumn)}`;
224
+ ${graphRouteResultSelect(rankedRelation, route.input, route.edgeIdColumn, false, false, weight !== undefined)}`;
215
225
  }
216
- function temporalShortestPathSelect(route, direction, temporal, metrics) {
226
+ function temporalShortestPathSelect(route, direction, temporal, metrics, includeTotal) {
217
227
  const prepared = route.traversal;
218
228
  const q = quoteIdentifier;
219
229
  const startsSelect = `SELECT ${q("start")},
@@ -226,7 +236,7 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
226
236
  : buildGraphTemporalCostStateSql(prepared, startsSelect, direction, temporal, route.distanceType, route.edgeWeight, [
227
237
  `${route.typedEdgeId} AS ${q("__edge_id")}`,
228
238
  `${route.edgeKey} AS ${q("__edge_key")}`,
229
- ]);
239
+ ], metrics.minimize === "steps" ? "steps" : "weight");
230
240
  const relations = prepared.relationNames([
231
241
  "graph_route_endpoints",
232
242
  "graph_nodes",
@@ -256,15 +266,21 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
256
266
  const routeAnchor = `${q("routes")}.${q("__journey_anchor")}`;
257
267
  const firstElapsed = temporal.journeyElapsed("edges", firstAnchor, direction);
258
268
  const candidateElapsed = temporal.journeyElapsed("edges", routeAnchor, direction);
259
- const firstMetric = metrics.minimize === "elapsedTime"
260
- ? firstElapsed
261
- : edgeCost;
262
- const candidateMetric = metrics.minimize === "elapsedTime"
263
- ? candidateElapsed
264
- : candidateDistance;
265
- const routeMetric = metrics.minimize === "elapsedTime"
266
- ? `${q("routes")}.${q("__elapsed")}`
267
- : routeDistance;
269
+ const firstMetric = metrics.minimize === "steps"
270
+ ? "1"
271
+ : metrics.minimize === "elapsedTime"
272
+ ? firstElapsed
273
+ : edgeCost;
274
+ const routeMetric = metrics.minimize === "steps"
275
+ ? `len(${q("routes")}.${q("steps")})`
276
+ : metrics.minimize === "elapsedTime"
277
+ ? `${q("routes")}.${q("__elapsed")}`
278
+ : routeDistance;
279
+ const candidateMetric = metrics.minimize === "steps"
280
+ ? `${routeMetric} + 1`
281
+ : metrics.minimize === "elapsedTime"
282
+ ? candidateElapsed
283
+ : candidateDistance;
268
284
  const step = (distance, elapsed) => `struct_pack(
269
285
  ${q("edgeId")} := ${q("edges")}.${q("__edge_id")},
270
286
  ${q("source")} := ${q("edges")}.${q("__from")},
@@ -297,7 +313,7 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
297
313
  INNER JOIN ${edgesRelation} AS ${q("edges")}
298
314
  ON ${q("reached")}.${q("__key")} = ${edgeToKey}
299
315
  ), ${bestTotalRelation} AS (
300
- SELECT MIN(${q("costs")}.${q("distance")}) AS ${q("distance")}
316
+ SELECT MIN(${q("costs")}.${q(metrics.minimize === "steps" ? "steps" : "distance")}) AS ${q("distance")}
301
317
  FROM ${costs.costRelation} AS ${q("costs")}
302
318
  INNER JOIN ${endpointRelation}
303
319
  ON ${q("costs")}.${q("__node_key")} = ${q("__end_key")}
@@ -352,7 +368,7 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
352
368
  AS ${q("pathId")}, ${q("steps")}
353
369
  FROM ${shortestRelation}
354
370
  )
355
- ${graphRouteResultSelect(rankedRelation, route.input, route.edgeIdColumn, false, metrics.elapsedTime)}`;
371
+ ${graphRouteResultSelect(rankedRelation, route.input, route.edgeIdColumn, false, metrics.elapsedTime, includeTotal)}`;
356
372
  }
357
373
  // At the same physical event a later departure (or earlier incoming arrival)
358
374
  // dominates every future elapsed duration. Keyed improvements find the optimum