@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
@@ -98,7 +98,10 @@ function distances(simpleTable, sourceColumn, targetColumn, startNodes, options
98
98
  return {
99
99
  start: validated.idType,
100
100
  node: validated.idType,
101
- total: validated.distanceType,
101
+ steps: "BIGINT",
102
+ ...(options.weight === undefined
103
+ ? {}
104
+ : { total: validated.distanceType }),
102
105
  ...(metrics.elapsedTime ? { elapsedTimeMs: "DOUBLE" } : {}),
103
106
  };
104
107
  },
@@ -123,12 +126,14 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
123
126
  const edgeWeight = weightColumn === undefined
124
127
  ? `CAST(1 AS ${distanceType})`
125
128
  : `CAST(${(0, quoteIdentifier_js_1.default)("edges")}.${(0, quoteIdentifier_js_1.default)(weightColumn.column)} AS ${distanceType})`;
126
- if (temporalOptions !== undefined) {
127
- const temporal = (0, prepareGraphTemporalSql_js_1.default)(schema, temporalOptions, "distances()");
128
- if (metrics.elapsedTime) {
129
- return elapsedDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, metrics.minimize);
130
- }
131
- return temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight);
129
+ const temporal = temporalOptions === undefined
130
+ ? undefined
131
+ : (0, prepareGraphTemporalSql_js_1.default)(schema, temporalOptions, "distances()");
132
+ if (weight !== undefined || metrics.elapsedTime) {
133
+ return summaryDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, metrics, weight !== undefined, weightColumn?.type === "FLOAT" || weightColumn?.type === "DOUBLE");
134
+ }
135
+ if (temporal !== undefined) {
136
+ return temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, "steps", false);
132
137
  }
133
138
  const relations = prepared.relationNames([
134
139
  "graph_start_values",
@@ -152,6 +157,14 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
152
157
  const edgeCost = `${(0, quoteIdentifier_js_1.default)("edges")}.${(0, quoteIdentifier_js_1.default)("__weight")}`;
153
158
  const bestDistance = `${(0, quoteIdentifier_js_1.default)("best")}.${(0, quoteIdentifier_js_1.default)("distance")}`;
154
159
  const candidateDistance = `CAST(${reachedDistance} + ${edgeCost} AS ${distanceType})`;
160
+ const candidateSteps = `${(0, quoteIdentifier_js_1.default)("reached")}.${(0, quoteIdentifier_js_1.default)("steps")} + 1`;
161
+ const bestSteps = `${(0, quoteIdentifier_js_1.default)("best")}.${(0, quoteIdentifier_js_1.default)("steps")}`;
162
+ const order = (distance, steps) => metrics.minimize === "steps"
163
+ ? `ROW(${steps}, ${distance})`
164
+ : `ROW(${distance}, ${steps})`;
165
+ const candidateOrder = order(candidateDistance, candidateSteps);
166
+ const selectedDistance = `arg_min(${candidateDistance}, ${candidateOrder})`;
167
+ const selectedSteps = `arg_min(${candidateSteps}, ${candidateOrder})`;
155
168
  return `WITH RECURSIVE ${startValuesRelation}(${(0, quoteIdentifier_js_1.default)("start")}) AS (
156
169
  VALUES ${prepared.startValues}
157
170
  ), ${startsRelation} AS (
@@ -165,19 +178,20 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
165
178
  ), ${distancesRelation}(
166
179
  ${(0, quoteIdentifier_js_1.default)("start")}, ${(0, quoteIdentifier_js_1.default)("node")},
167
180
  ${(0, quoteIdentifier_js_1.default)("__start_key")}, ${(0, quoteIdentifier_js_1.default)("__node_key")},
168
- ${(0, quoteIdentifier_js_1.default)("distance")}
181
+ ${(0, quoteIdentifier_js_1.default)("distance")}, ${(0, quoteIdentifier_js_1.default)("steps")}
169
182
  ) USING KEY(
170
183
  ${(0, quoteIdentifier_js_1.default)("__start_key")}, ${(0, quoteIdentifier_js_1.default)("__node_key")}
171
184
  ) AS (
172
185
  SELECT ${start}, ${edgeTo}, ${startKey}, ${edgeToKey},
173
- MIN(${edgeCost}) AS ${(0, quoteIdentifier_js_1.default)("distance")}
186
+ MIN(${edgeCost}) AS ${(0, quoteIdentifier_js_1.default)("distance")}, CAST(1 AS BIGINT)
174
187
  FROM ${startsRelation} AS ${(0, quoteIdentifier_js_1.default)("starts")}
175
188
  INNER JOIN ${edgesRelation} AS ${(0, quoteIdentifier_js_1.default)("edges")}
176
189
  ON ${startKey} = ${edgeFromKey}
177
190
  GROUP BY ${start}, ${edgeTo}, ${startKey}, ${edgeToKey}
178
191
  UNION
179
192
  SELECT ${reachedStart}, ${edgeTo}, ${reachedStartKey}, ${edgeToKey},
180
- MIN(${candidateDistance}) AS ${(0, quoteIdentifier_js_1.default)("distance")}
193
+ ${selectedDistance} AS ${(0, quoteIdentifier_js_1.default)("distance")},
194
+ ${selectedSteps} AS ${(0, quoteIdentifier_js_1.default)("steps")}
181
195
  FROM ${distancesRelation} AS ${(0, quoteIdentifier_js_1.default)("reached")}
182
196
  INNER JOIN ${edgesRelation} AS ${(0, quoteIdentifier_js_1.default)("edges")}
183
197
  ON ${reachedNodeKey} = ${edgeFromKey}
@@ -185,111 +199,259 @@ function distancesSelect(input, schema, source, target, starts, direction, weigh
185
199
  ON ${reachedStartKey} = ${(0, quoteIdentifier_js_1.default)("best")}.${(0, quoteIdentifier_js_1.default)("__start_key")}
186
200
  AND ${edgeToKey} = ${(0, quoteIdentifier_js_1.default)("best")}.${(0, quoteIdentifier_js_1.default)("__node_key")}
187
201
  GROUP BY ${reachedStart}, ${edgeTo}, ${reachedStartKey}, ${edgeToKey},
188
- ${bestDistance}
189
- HAVING ${bestDistance} IS NULL OR MIN(${candidateDistance}) < ${bestDistance}
202
+ ${bestDistance}, ${bestSteps}
203
+ HAVING ${bestDistance} IS NULL OR
204
+ ${order(selectedDistance, selectedSteps)} < ${order(bestDistance, bestSteps)}
190
205
  )
191
206
  SELECT ${(0, quoteIdentifier_js_1.default)("start")}, ${(0, quoteIdentifier_js_1.default)("node")},
192
- ${(0, quoteIdentifier_js_1.default)("distance")} AS ${(0, quoteIdentifier_js_1.default)("total")}
207
+ ${(0, quoteIdentifier_js_1.default)("steps")}${weight === undefined
208
+ ? ""
209
+ : `, ${(0, quoteIdentifier_js_1.default)("distance")} AS ${(0, quoteIdentifier_js_1.default)("total")}`}
193
210
  FROM ${distancesRelation}
194
- ORDER BY ${(0, quoteIdentifier_js_1.default)("__start_key")}, ${(0, quoteIdentifier_js_1.default)("distance")}, ${(0, quoteIdentifier_js_1.default)("__node_key")}`;
211
+ ORDER BY ${(0, quoteIdentifier_js_1.default)("__start_key")}, ${(0, quoteIdentifier_js_1.default)(metrics.minimize === "steps" ? "steps" : "distance")}, ${(0, quoteIdentifier_js_1.default)("__node_key")}`;
195
212
  }
196
- function temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight) {
213
+ function temporalDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, minimize, includeTotal) {
197
214
  const q = quoteIdentifier_js_1.default;
198
215
  const startsSelect = `SELECT ${q("start")},
199
216
  ${prepared.key(q("start"))} AS ${q("__key")}
200
217
  FROM (VALUES ${prepared.startValues}) AS ${q("start_values")}(${q("start")})`;
201
- const costStates = (0, buildGraphTemporalCostStateSql_js_1.default)(prepared, startsSelect, direction, temporal, distanceType, edgeWeight);
218
+ const costStates = (0, buildGraphTemporalCostStateSql_js_1.default)(prepared, startsSelect, direction, temporal, distanceType, edgeWeight, [], minimize);
219
+ const order = minimize === "steps"
220
+ ? `${q("steps")}, ${q("distance")}`
221
+ : `${q("distance")}, ${q("steps")}`;
202
222
  return `${costStates.withClause}
203
- SELECT ${q("start")}, ${q("node")},
204
- MIN(${q("distance")}) AS ${q("total")}
223
+ SELECT ${q("start")}, ${q("node")}, ${q("steps")}${includeTotal ? `, ${q("distance")} AS ${q("total")}` : ""}
205
224
  FROM ${costStates.costRelation}
206
- GROUP BY ${q("start")}, ${q("node")}, ${q("__start_key")},
207
- ${q("__node_key")}
208
- ORDER BY ${q("__start_key")}, ${q("total")}, ${q("__node_key")}`;
225
+ QUALIFY row_number() OVER (
226
+ PARTITION BY ${q("__start_key")}, ${q("__node_key")}
227
+ ORDER BY ${order}
228
+ ) = 1
229
+ ORDER BY ${q("__start_key")}, ${q(minimize === "steps" ? "steps" : "distance")}, ${q("__node_key")}`;
209
230
  }
210
- function elapsedDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, minimize) {
231
+ // Distinct visited sets, metrics, and temporal boundaries are enough to extend
232
+ // summaries. No edge IDs or full connection sequences are needed. Keeping the
233
+ // visited set is necessary: equal summaries can have different legal extensions.
234
+ function summaryDistancesSelect(prepared, direction, temporal, distanceType, edgeWeight, metrics, includeTotal, floatingWeight) {
211
235
  const q = quoteIdentifier_js_1.default;
212
236
  const relations = prepared.relationNames([
213
237
  "graph_starts",
214
238
  "graph_temporal_settings",
215
239
  "graph_event_rows",
216
240
  "graph_edges",
217
- "graph_elapsed_states",
218
- "graph_elapsed_results",
241
+ "graph_summaries",
242
+ "graph_metric_results",
243
+ "graph_tied_summaries",
244
+ "graph_best_metrics",
245
+ "graph_metric_bounds",
219
246
  ]);
220
247
  const starts = relations.graph_starts;
221
248
  const settings = relations.graph_temporal_settings;
222
249
  const events = relations.graph_event_rows;
223
250
  const edges = relations.graph_edges;
224
- const states = relations.graph_elapsed_states;
225
- const results = relations.graph_elapsed_results;
251
+ const summaries = relations.graph_summaries;
252
+ const results = relations.graph_metric_results;
253
+ const tied = relations.graph_tied_summaries;
226
254
  const ref = (alias, column) => `${q(alias)}.${q(column)}`;
227
- const anchor = temporal.journeyAnchor("edges", direction);
228
- const reachedAnchor = ref("reached", "__anchor");
229
- const candidateDistance = `CAST(${ref("reached", "distance")} + ${ref("edges", "__weight")} AS ${distanceType})`;
230
- const order = minimize === "elapsedTime"
231
- ? `${q("__elapsed")}, ${q("distance")}`
232
- : `${q("distance")}, ${q("__elapsed")}`;
233
- // A physical event and the first departure (last arrival for incoming
234
- // traversal) fix elapsed time. Keep the cheapest cost for each such state;
235
- // merging different anchors would lose later-departing, faster journeys.
236
- // The state space is finite, and strict improvements terminate zero cycles.
237
- return `WITH RECURSIVE ${settings} AS MATERIALIZED (
238
- SELECT CAST(? AS HUGEINT) AS ${q("__gap")}
255
+ const searchDirection = direction;
256
+ const firstAnchor = temporal?.journeyAnchor("edges", searchDirection);
257
+ const sum = `CAST(${ref("reached", "distance")} + ${ref("edges", "__weight")} AS ${distanceType})`;
258
+ // Reported costs must not reject routes before the primary objective has
259
+ // selected its results. NULL marks overflow and propagates through sums;
260
+ // non-negative weights cannot bring an overflowing total back into range.
261
+ const nextDistance = metrics.minimize === "weight" ? sum : `TRY(${sum})`;
262
+ const temporalColumns = temporal === undefined
263
+ ? ""
264
+ : `, ${q("__event_start")}, ${q("__event_end")}`;
265
+ const eventValues = temporal === undefined
266
+ ? ""
267
+ : `, ${ref("edges", "__event_start")}, ${ref("edges", "__event_end")}`;
268
+ const metric = metrics.minimize === "steps"
269
+ ? q("steps")
270
+ : metrics.minimize === "weight"
271
+ ? q("distance")
272
+ : q("__elapsed");
273
+ // Use native time precision for selection. Collapse equal reported values
274
+ // after conversion to milliseconds, since these are summary rows.
275
+ const reportedElapsed = metrics.elapsedTime
276
+ ? `, ${temporal.elapsedMilliseconds(q("__elapsed"))} AS ${q("elapsedTimeMs")}`
277
+ : "";
278
+ const order = metrics.minimize === "steps"
279
+ ? [
280
+ q("steps"),
281
+ q("__node_key"),
282
+ ...(includeTotal ? [q("total")] : []),
283
+ ...(metrics.elapsedTime ? [q("elapsedTimeMs")] : []),
284
+ ]
285
+ : metrics.minimize === "weight"
286
+ ? [
287
+ q("total"),
288
+ q("__node_key"),
289
+ q("steps"),
290
+ ...(metrics.elapsedTime ? [q("elapsedTimeMs")] : []),
291
+ ]
292
+ : [
293
+ q("elapsedTimeMs"),
294
+ q("__node_key"),
295
+ q("steps"),
296
+ ...(includeTotal ? [q("total")] : []),
297
+ ];
298
+ const bounds = summaryBoundsSql(relations.graph_best_metrics, relations.graph_metric_bounds, starts, edges, settings, temporal, searchDirection, metrics.minimize, distanceType, floatingWeight);
299
+ return `WITH RECURSIVE ${settings} AS (
300
+ ${temporal === undefined
301
+ ? `SELECT 0 AS ${q("__gap")}`
302
+ : `SELECT CAST(? AS HUGEINT) AS ${q("__gap")}`}
239
303
  ), ${events} AS MATERIALIZED (
240
304
  ${prepared.edges(direction, [
241
305
  `${edgeWeight} AS ${q("__weight")}`,
242
- ...temporal.eventSelections("edges"),
306
+ ...(temporal?.eventSelections("edges") ?? []),
243
307
  ])}
244
308
  ), ${edges} AS MATERIALIZED (
245
- SELECT * FROM ${events} AS ${q("events")}
246
- WHERE ${temporal.eventValidity("events")}
247
- ), ${starts} AS MATERIALIZED (
309
+ SELECT DISTINCT ${q("__from")}, ${q("__to")}, ${q("__from_key")},
310
+ ${q("__to_key")}, ${q("__weight")}${temporalColumns}
311
+ FROM ${events} AS ${q("events")}
312
+ ${temporal === undefined ? "" : `WHERE ${temporal.eventValidity("events")}`}
313
+ ), ${starts} AS (
248
314
  SELECT ${q("start")}, ${prepared.key(q("start"))} AS ${q("__key")}
249
315
  FROM (VALUES ${prepared.startValues}) AS ${q("start_values")}(${q("start")})
250
- ), ${states}(
316
+ ), ${bounds.ctes}, ${summaries}(
251
317
  ${q("start")}, ${q("node")}, ${q("__start_key")}, ${q("__node_key")},
252
- ${q("__event_id")}, ${q("__event_start")}, ${q("__event_end")},
253
- ${q("__anchor")}, ${q("distance")}
254
- ) USING KEY(${q("__start_key")}, ${q("__event_id")}, ${q("__anchor")}) AS (
318
+ ${q("__visited")}, ${q("steps")}, ${q("distance")}${temporalColumns}${metrics.elapsedTime ? `, ${q("__anchor")}` : ""}
319
+ ) AS (
255
320
  SELECT ${ref("starts", "start")}, ${ref("edges", "__to")},
256
321
  ${ref("starts", "__key")}, ${ref("edges", "__to_key")},
257
- ${ref("edges", "__event_id")}, ${ref("edges", "__event_start")},
258
- ${ref("edges", "__event_end")}, ${anchor}, ${ref("edges", "__weight")}
322
+ list_sort(list_distinct([${ref("starts", "__key")}, ${ref("edges", "__to_key")}])),
323
+ CAST(1 AS BIGINT), ${ref("edges", "__weight")}${eventValues}${metrics.elapsedTime ? `, ${firstAnchor}` : ""}
259
324
  FROM ${starts} AS ${q("starts")}
260
325
  INNER JOIN ${edges} AS ${q("edges")}
261
326
  ON ${ref("starts", "__key")} = ${ref("edges", "__from_key")}
327
+ ${bounds.join(ref("starts", "__key"))}
328
+ WHERE ${bounds.test("CAST(1 AS BIGINT)", ref("edges", "__weight"), firstAnchor)}
262
329
  UNION
263
330
  SELECT ${ref("reached", "start")}, ${ref("edges", "__to")},
264
331
  ${ref("reached", "__start_key")}, ${ref("edges", "__to_key")},
265
- ${ref("edges", "__event_id")}, ${ref("edges", "__event_start")},
266
- ${ref("edges", "__event_end")}, ${reachedAnchor},
267
- MIN(${candidateDistance}) AS ${q("distance")}
268
- FROM ${states} AS ${q("reached")}
332
+ list_sort(list_distinct(list_append(${ref("reached", "__visited")}, ${ref("edges", "__to_key")}))),
333
+ ${ref("reached", "steps")} + 1, ${nextDistance}${eventValues}${metrics.elapsedTime ? `, ${ref("reached", "__anchor")}` : ""}
334
+ FROM ${summaries} AS ${q("reached")}
269
335
  INNER JOIN ${edges} AS ${q("edges")}
270
336
  ON ${ref("reached", "__node_key")} = ${ref("edges", "__from_key")}
271
337
  CROSS JOIN ${settings} AS ${q("settings")}
272
- LEFT JOIN recurring.${states} AS ${q("best")}
273
- ON ${ref("reached", "__start_key")} = ${ref("best", "__start_key")}
274
- AND ${ref("edges", "__event_id")} = ${ref("best", "__event_id")}
275
- AND ${reachedAnchor} = ${ref("best", "__anchor")}
276
- WHERE ${temporal.transition("reached", "edges", direction, ref("settings", "__gap"))}
277
- GROUP BY ${ref("reached", "start")}, ${ref("edges", "__to")},
278
- ${ref("reached", "__start_key")}, ${ref("edges", "__to_key")},
279
- ${ref("edges", "__event_id")}, ${ref("edges", "__event_start")},
280
- ${ref("edges", "__event_end")}, ${reachedAnchor}, ${ref("best", "distance")}
281
- HAVING ${ref("best", "distance")} IS NULL OR
282
- MIN(${candidateDistance}) < ${ref("best", "distance")}
338
+ ${bounds.join(ref("reached", "__start_key"))}
339
+ WHERE ${bounds.test(`${ref("reached", "steps")} + 1`, nextDistance, ref("reached", "__anchor"))}
340
+ AND ${ref("reached", "__node_key")} <> ${ref("reached", "__start_key")}
341
+ AND (NOT list_contains(${ref("reached", "__visited")}, ${ref("edges", "__to_key")})
342
+ OR ${ref("edges", "__to_key")} = ${ref("reached", "__start_key")})
343
+ ${temporal === undefined ? "" : `AND ${temporal.transition("reached", "edges", searchDirection, ref("settings", "__gap"))}`}
283
344
  ), ${results} AS (
284
- SELECT *, ${temporal.journeyElapsed("states", ref("states", "__anchor"), direction)} AS ${q("__elapsed")}
285
- FROM ${states} AS ${q("states")}
345
+ SELECT *${metrics.elapsedTime
346
+ ? `, ${temporal.journeyElapsed("summaries", ref("summaries", "__anchor"), searchDirection)} AS ${q("__elapsed")}`
347
+ : ""}
348
+ FROM ${summaries} AS ${q("summaries")}
349
+ ), ${tied} AS MATERIALIZED (
350
+ SELECT DISTINCT ${q("start")}, ${q("node")}, ${q("__start_key")}, ${q("__node_key")},
351
+ ${q("steps")}${includeTotal ? `, ${q("distance")} AS ${q("total")}` : ""}${reportedElapsed}
352
+ FROM ${results}
353
+ QUALIFY ${metric} = MIN(${metric}) OVER (
354
+ PARTITION BY ${q("__start_key")}, ${q("__node_key")}
355
+ )
286
356
  )
287
- SELECT ${q("start")}, ${q("node")}, ${q("distance")} AS ${q("total")},
288
- ${temporal.elapsedMilliseconds(q("__elapsed"))} AS ${q("elapsedTimeMs")}
289
- FROM ${results}
290
- QUALIFY row_number() OVER (
291
- PARTITION BY ${q("__start_key")}, ${q("__node_key")}
292
- ORDER BY ${order}, ${q("__event_id")}, ${q("__anchor")}
293
- ) = 1
294
- ORDER BY ${q("__start_key")}, ${order}, ${q("__node_key")}`;
357
+ SELECT ${q("start")}, ${q("node")}, ${q("steps")}${includeTotal
358
+ ? `, CASE WHEN ${q("total")} IS NULL
359
+ THEN error('distances() total exceeds the supported ${distanceType} range for an optimal route.')
360
+ ELSE ${q("total")} END AS ${q("total")}`
361
+ : ""}${metrics.elapsedTime ? `, ${q("elapsedTimeMs")}` : ""}
362
+ FROM ${tied}
363
+ ORDER BY ${q("__start_key")}, ${order.join(", ")}`;
364
+ }
365
+ // Compute primary optima without enumerating visited sets. Exact additive
366
+ // objectives have optimal prefixes (at the same event boundary in temporal
367
+ // traversal). Floating addition can erase prefix differences, and elapsed
368
+ // optima can depend on a later departure, so those use a per-start upper bound.
369
+ function summaryBoundsSql(best, limits, starts, edges, settings, temporal, direction, minimize, distanceType, floatingWeight) {
370
+ const q = quoteIdentifier_js_1.default;
371
+ const ref = (alias, column) => `${q(alias)}.${q(column)}`;
372
+ const elapsed = minimize === "elapsedTime";
373
+ const globalBound = elapsed || (minimize === "weight" && floatingWeight);
374
+ const boundaries = temporal === undefined
375
+ ? []
376
+ : ["__event_start", "__event_end"];
377
+ const keys = [
378
+ "__start_key",
379
+ "__node_key",
380
+ ...boundaries,
381
+ ...(elapsed ? ["__anchor"] : []),
382
+ ];
383
+ const firstAnchor = elapsed
384
+ ? temporal.journeyAnchor("edges", direction)
385
+ : undefined;
386
+ const firstKeys = [
387
+ ref("starts", "__key"),
388
+ ref("edges", "__to_key"),
389
+ ...boundaries.map((key) => ref("edges", key)),
390
+ ...(elapsed ? [firstAnchor] : []),
391
+ ];
392
+ const nextKeys = [
393
+ ref("reached", "__start_key"),
394
+ ref("edges", "__to_key"),
395
+ ...boundaries.map((key) => ref("edges", key)),
396
+ ...(elapsed ? [ref("reached", "__anchor")] : []),
397
+ ];
398
+ const firstMetric = minimize === "steps"
399
+ ? "CAST(1 AS BIGINT)"
400
+ : elapsed
401
+ ? temporal.journeyElapsed("edges", firstAnchor, direction)
402
+ : ref("edges", "__weight");
403
+ const nextMetric = minimize === "steps"
404
+ ? `${ref("reached", "__metric")} + 1`
405
+ : elapsed
406
+ ? temporal.journeyElapsed("edges", ref("reached", "__anchor"), direction)
407
+ : `CAST(${ref("reached", "__metric")} + ${ref("edges", "__weight")} AS ${distanceType})`;
408
+ const minimum = `MIN(${nextMetric})`;
409
+ const bestMetric = ref("best", "__metric");
410
+ const ctes = `${best}(${keys.map(q).join(", ")}, ${q("__metric")})
411
+ USING KEY(${keys.map(q).join(", ")}) AS (
412
+ SELECT ${firstKeys.join(", ")}, MIN(${firstMetric})
413
+ FROM ${starts} AS ${q("starts")}
414
+ INNER JOIN ${edges} AS ${q("edges")}
415
+ ON ${ref("starts", "__key")} = ${ref("edges", "__from_key")}
416
+ GROUP BY ${firstKeys.join(", ")}
417
+ UNION
418
+ SELECT ${nextKeys.join(", ")}, ${minimum}
419
+ FROM ${best} AS ${q("reached")}
420
+ INNER JOIN ${edges} AS ${q("edges")}
421
+ ON ${ref("reached", "__node_key")} = ${ref("edges", "__from_key")}
422
+ CROSS JOIN ${settings} AS ${q("settings")}
423
+ LEFT JOIN recurring.${best} AS ${q("best")}
424
+ ON ${keys.map((key, i) => `${ref("best", key)} = ${nextKeys[i]}`).join(" AND ")}
425
+ WHERE ${ref("reached", "__node_key")} <> ${ref("reached", "__start_key")}
426
+ ${temporal === undefined ? "" : `AND ${temporal.transition("reached", "edges", direction, ref("settings", "__gap"))}`}
427
+ GROUP BY ${nextKeys.join(", ")}, ${bestMetric}
428
+ HAVING ${bestMetric} IS NULL OR ${minimum} < ${bestMetric}
429
+ )${globalBound
430
+ ? `, ${limits} AS (
431
+ SELECT ${q("__start_key")}, MAX(${q("__minimum")}) AS ${q("__metric")}
432
+ FROM (
433
+ SELECT ${q("__start_key")}, ${q("__node_key")}, MIN(${q("__metric")}) AS ${q("__minimum")}
434
+ FROM ${best}
435
+ GROUP BY ${q("__start_key")}, ${q("__node_key")}
436
+ ) AS ${q("minima")}
437
+ GROUP BY ${q("__start_key")}
438
+ )`
439
+ : ""}`;
440
+ return {
441
+ ctes,
442
+ join: (startKey) => `INNER JOIN ${globalBound ? limits : best} AS ${q("bounds")}
443
+ ON ${ref("bounds", "__start_key")} = ${startKey}
444
+ ${globalBound
445
+ ? ""
446
+ : `AND ${ref("bounds", "__node_key")} = ${ref("edges", "__to_key")}
447
+ ${boundaries.map((key) => `AND ${ref("bounds", key)} = ${ref("edges", key)}`).join(" ")}`}`,
448
+ test: (steps, distance, anchor) => {
449
+ const metric = minimize === "steps"
450
+ ? steps
451
+ : elapsed
452
+ ? temporal.journeyElapsed("edges", anchor, direction)
453
+ : distance;
454
+ return `${metric} ${globalBound ? "<=" : "="} ${ref("bounds", "__metric")}`;
455
+ },
456
+ };
295
457
  }
@@ -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"}
@@ -92,7 +92,7 @@ function shortestPath(simpleTable, sourceColumn, targetColumn, edgeId, start, en
92
92
  ? undefined
93
93
  : (input) => (0, validateGraphTemporalEvents_js_1.default)(input, temporalOptions, "shortestPath()", parameters),
94
94
  values: (schema) => {
95
- (0, prepareGraphRouteSql_js_1.validateGraphRouteInputs)(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()");
95
+ (0, prepareGraphRouteSql_js_1.validateGraphRouteInputs)(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()", options.weight !== undefined);
96
96
  const temporal = temporalOptions === undefined
97
97
  ? undefined
98
98
  : (0, prepareGraphTemporalSql_js_1.default)(schema, temporalOptions, "shortestPath()");
@@ -102,27 +102,28 @@ function shortestPath(simpleTable, sourceColumn, targetColumn, edgeId, start, en
102
102
  },
103
103
  buildSelect: (input, schema) => shortestPathSelect(input, schema, sourceColumn, targetColumn, edgeId, endpoints, direction, options.weight, temporalOptions, metrics),
104
104
  outputSchema: (schema) => {
105
- const validated = (0, prepareGraphRouteSql_js_1.validateGraphRouteInputs)(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()");
105
+ const validated = (0, prepareGraphRouteSql_js_1.validateGraphRouteInputs)(schema, sourceColumn, targetColumn, edgeId, endpoints, options.weight, "shortestPath()", options.weight !== undefined);
106
106
  if (temporalOptions !== undefined) {
107
107
  (0, prepareGraphTemporalSql_js_1.default)(schema, temporalOptions, "shortestPath()");
108
108
  }
109
- return (0, graphRouteResultSchema_js_1.default)(schema, validated.distanceType, "shortestPath()", metrics.elapsedTime);
109
+ return (0, graphRouteResultSchema_js_1.default)(schema, validated.distanceType, "shortestPath()", metrics.elapsedTime, options.weight !== undefined);
110
110
  },
111
111
  });
112
112
  }
113
113
  function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoints, direction, weight, temporalOptions, metrics) {
114
- const route = (0, prepareGraphRouteSql_js_1.prepareGraphRouteSql)(input, schema, source, target, edgeId, routeEndpoints, weight, "shortestPath()");
114
+ const route = (0, prepareGraphRouteSql_js_1.prepareGraphRouteSql)(input, schema, source, target, edgeId, routeEndpoints, weight, "shortestPath()", weight !== undefined);
115
115
  const prepared = route.traversal;
116
116
  if (temporalOptions !== undefined) {
117
117
  const temporal = (0, prepareGraphTemporalSql_js_1.default)(schema, temporalOptions, "shortestPath()");
118
- return temporalShortestPathSelect(route, direction, temporal, metrics);
118
+ return temporalShortestPathSelect(route, direction, temporal, metrics, weight !== undefined);
119
119
  }
120
120
  const distanceType = route.distanceType;
121
121
  // Exact shortest routes have optimal prefixes. Floating addition can erase
122
122
  // a prefix-cost difference later, so retain all simple prefixes up to the
123
123
  // best complete cost before selecting the full-route ties.
124
- const floating = route.weightType === "FLOAT" ||
125
- route.weightType === "DOUBLE";
124
+ const floating = metrics.minimize === "weight" &&
125
+ (route.weightType === "FLOAT" || route.weightType === "DOUBLE");
126
+ const metricType = metrics.minimize === "steps" ? "BIGINT" : distanceType;
126
127
  const relations = prepared.relationNames([
127
128
  "graph_route_endpoints",
128
129
  "graph_edges",
@@ -148,8 +149,17 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
148
149
  const edgeToKey = `${q("edges")}.${q("__to_key")}`;
149
150
  const edgeTo = `${q("edges")}.${q("__to")}`;
150
151
  const edgeCost = `${q("edges")}.${q("__weight")}`;
152
+ const edgeMetric = metrics.minimize === "steps"
153
+ ? "CAST(1 AS BIGINT)"
154
+ : edgeCost;
151
155
  const routeDistance = `${q("routes")}.${q("distance")}`;
152
156
  const candidateDistance = `CAST(${routeDistance} + ${edgeCost} AS ${distanceType})`;
157
+ const routeMetric = metrics.minimize === "steps"
158
+ ? `len(${q("routes")}.${q("steps")})`
159
+ : routeDistance;
160
+ const candidateMetric = metrics.minimize === "steps"
161
+ ? `${routeMetric} + 1`
162
+ : candidateDistance;
153
163
  return `WITH RECURSIVE ${endpointRelation}(
154
164
  ${q("start")}, ${q("end")}, ${q("__start_key")}, ${q("__end_key")}
155
165
  ) AS (
@@ -181,13 +191,13 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
181
191
  ${q("node")}, ${q("__node_key")}, ${q("distance")}
182
192
  ) USING KEY(${q("__node_key")}) AS (
183
193
  SELECT ${q("start")}, ${q("__start_key")},
184
- CAST(0 AS ${distanceType})
194
+ CAST(0 AS ${metricType})
185
195
  FROM ${endpointRelation}
186
196
  INNER JOIN ${toEndRelation}
187
197
  ON ${q("__start_key")} = ${q("__key")}
188
198
  UNION
189
199
  SELECT ${edgeTo}, ${edgeToKey},
190
- MIN(CAST(${q("reached")}.${q("distance")} + ${edgeCost} AS ${distanceType}))
200
+ MIN(CAST(${q("reached")}.${q("distance")} + ${edgeMetric} AS ${metricType}))
191
201
  FROM ${distancesRelation} AS ${q("reached")}
192
202
  INNER JOIN ${edgesRelation} AS ${q("edges")}
193
203
  ON ${q("reached")}.${q("__node_key")} = ${edgeFromKey}
@@ -197,7 +207,7 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
197
207
  ON ${edgeToKey} = ${q("best")}.${q("__node_key")}
198
208
  GROUP BY ${edgeTo}, ${edgeToKey}, ${q("best")}.${q("distance")}
199
209
  HAVING ${q("best")}.${q("distance")} IS NULL
200
- OR MIN(CAST(${q("reached")}.${q("distance")} + ${edgeCost} AS ${distanceType})) < ${q("best")}.${q("distance")}
210
+ OR MIN(CAST(${q("reached")}.${q("distance")} + ${edgeMetric} AS ${metricType})) < ${q("best")}.${q("distance")}
201
211
  ), ${bestTotalRelation} AS (
202
212
  SELECT ${q("distances")}.${q("distance")}
203
213
  FROM ${distancesRelation} AS ${q("distances")}
@@ -236,23 +246,23 @@ function shortestPathSelect(input, schema, source, target, edgeId, routeEndpoint
236
246
  WHERE ${q("routes")}.${q("__node_key")} <> ${q("__end_key")}
237
247
  AND NOT list_contains(${q("routes")}.${q("__visited")}, ${edgeToKey})
238
248
  AND ${floating
239
- ? `${candidateDistance} <= ${q("best_total")}.${q("distance")}`
240
- : `${candidateDistance} = ${q("best_prefix")}.${q("distance")}`}
249
+ ? `${candidateMetric} <= ${q("best_total")}.${q("distance")}`
250
+ : `${candidateMetric} = ${q("best_prefix")}.${q("distance")}`}
241
251
  ), ${shortestRelation} AS (
242
252
  SELECT ${q("__edge_keys")}, ${q("steps")}
243
- FROM ${routesRelation}
253
+ FROM ${routesRelation} AS ${q("routes")}
244
254
  CROSS JOIN ${endpointRelation}
245
255
  CROSS JOIN ${bestTotalRelation} AS ${q("best_total")}
246
256
  WHERE ${q("__node_key")} = ${q("__end_key")}
247
- AND ${routesRelation}.${q("distance")} = ${q("best_total")}.${q("distance")}
257
+ AND ${routeMetric} = ${q("best_total")}.${q("distance")}
248
258
  ), ${rankedRelation} AS (
249
259
  SELECT CAST(row_number() OVER (ORDER BY ${q("__edge_keys")}) - 1 AS BIGINT) AS ${q("pathId")},
250
260
  ${q("steps")}
251
261
  FROM ${shortestRelation}
252
262
  )
253
- ${(0, graphRouteResultSelect_js_1.default)(rankedRelation, route.input, route.edgeIdColumn)}`;
263
+ ${(0, graphRouteResultSelect_js_1.default)(rankedRelation, route.input, route.edgeIdColumn, false, false, weight !== undefined)}`;
254
264
  }
255
- function temporalShortestPathSelect(route, direction, temporal, metrics) {
265
+ function temporalShortestPathSelect(route, direction, temporal, metrics, includeTotal) {
256
266
  const prepared = route.traversal;
257
267
  const q = quoteIdentifier_js_1.default;
258
268
  const startsSelect = `SELECT ${q("start")},
@@ -265,7 +275,7 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
265
275
  : (0, buildGraphTemporalCostStateSql_js_1.default)(prepared, startsSelect, direction, temporal, route.distanceType, route.edgeWeight, [
266
276
  `${route.typedEdgeId} AS ${q("__edge_id")}`,
267
277
  `${route.edgeKey} AS ${q("__edge_key")}`,
268
- ]);
278
+ ], metrics.minimize === "steps" ? "steps" : "weight");
269
279
  const relations = prepared.relationNames([
270
280
  "graph_route_endpoints",
271
281
  "graph_nodes",
@@ -295,15 +305,21 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
295
305
  const routeAnchor = `${q("routes")}.${q("__journey_anchor")}`;
296
306
  const firstElapsed = temporal.journeyElapsed("edges", firstAnchor, direction);
297
307
  const candidateElapsed = temporal.journeyElapsed("edges", routeAnchor, direction);
298
- const firstMetric = metrics.minimize === "elapsedTime"
299
- ? firstElapsed
300
- : edgeCost;
301
- const candidateMetric = metrics.minimize === "elapsedTime"
302
- ? candidateElapsed
303
- : candidateDistance;
304
- const routeMetric = metrics.minimize === "elapsedTime"
305
- ? `${q("routes")}.${q("__elapsed")}`
306
- : routeDistance;
308
+ const firstMetric = metrics.minimize === "steps"
309
+ ? "1"
310
+ : metrics.minimize === "elapsedTime"
311
+ ? firstElapsed
312
+ : edgeCost;
313
+ const routeMetric = metrics.minimize === "steps"
314
+ ? `len(${q("routes")}.${q("steps")})`
315
+ : metrics.minimize === "elapsedTime"
316
+ ? `${q("routes")}.${q("__elapsed")}`
317
+ : routeDistance;
318
+ const candidateMetric = metrics.minimize === "steps"
319
+ ? `${routeMetric} + 1`
320
+ : metrics.minimize === "elapsedTime"
321
+ ? candidateElapsed
322
+ : candidateDistance;
307
323
  const step = (distance, elapsed) => `struct_pack(
308
324
  ${q("edgeId")} := ${q("edges")}.${q("__edge_id")},
309
325
  ${q("source")} := ${q("edges")}.${q("__from")},
@@ -336,7 +352,7 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
336
352
  INNER JOIN ${edgesRelation} AS ${q("edges")}
337
353
  ON ${q("reached")}.${q("__key")} = ${edgeToKey}
338
354
  ), ${bestTotalRelation} AS (
339
- SELECT MIN(${q("costs")}.${q("distance")}) AS ${q("distance")}
355
+ SELECT MIN(${q("costs")}.${q(metrics.minimize === "steps" ? "steps" : "distance")}) AS ${q("distance")}
340
356
  FROM ${costs.costRelation} AS ${q("costs")}
341
357
  INNER JOIN ${endpointRelation}
342
358
  ON ${q("costs")}.${q("__node_key")} = ${q("__end_key")}
@@ -391,7 +407,7 @@ function temporalShortestPathSelect(route, direction, temporal, metrics) {
391
407
  AS ${q("pathId")}, ${q("steps")}
392
408
  FROM ${shortestRelation}
393
409
  )
394
- ${(0, graphRouteResultSelect_js_1.default)(rankedRelation, route.input, route.edgeIdColumn, false, metrics.elapsedTime)}`;
410
+ ${(0, graphRouteResultSelect_js_1.default)(rankedRelation, route.input, route.edgeIdColumn, false, metrics.elapsedTime, includeTotal)}`;
395
411
  }
396
412
  // At the same physical event a later departure (or earlier incoming arrival)
397
413
  // dominates every future elapsed duration. Keyed improvements find the optimum