@uptimizr/db 0.8.2 → 1.0.1

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 (43) hide show
  1. package/bin/uptimizr-db-migrate.js +4 -0
  2. package/bin/uptimizr-db-new-project.js +4 -0
  3. package/dist/cli/createProject.js +0 -0
  4. package/dist/cli/migrate.js +0 -0
  5. package/dist/env.d.ts +57 -2
  6. package/dist/env.d.ts.map +1 -1
  7. package/dist/env.js +31 -2
  8. package/dist/env.js.map +1 -1
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/query/aggregations.d.ts +4 -4
  14. package/dist/query/aggregations.d.ts.map +1 -1
  15. package/dist/query/aggregations.js +200 -143
  16. package/dist/query/aggregations.js.map +1 -1
  17. package/dist/query/clickhouseDialect.d.ts.map +1 -1
  18. package/dist/query/clickhouseDialect.js +7 -2
  19. package/dist/query/clickhouseDialect.js.map +1 -1
  20. package/dist/query/dialect.d.ts +55 -11
  21. package/dist/query/dialect.d.ts.map +1 -1
  22. package/dist/query/dialect.js +6 -4
  23. package/dist/query/dialect.js.map +1 -1
  24. package/dist/query/duckdbDialect.d.ts.map +1 -1
  25. package/dist/query/duckdbDialect.js +7 -2
  26. package/dist/query/duckdbDialect.js.map +1 -1
  27. package/dist/query/index.d.ts +3 -0
  28. package/dist/query/index.d.ts.map +1 -1
  29. package/dist/query/index.js +6 -0
  30. package/dist/query/index.js.map +1 -1
  31. package/dist/query/mssqlDialect.d.ts +99 -0
  32. package/dist/query/mssqlDialect.d.ts.map +1 -0
  33. package/dist/query/mssqlDialect.js +398 -0
  34. package/dist/query/mssqlDialect.js.map +1 -0
  35. package/dist/query/postgresDialect.d.ts +48 -0
  36. package/dist/query/postgresDialect.d.ts.map +1 -0
  37. package/dist/query/postgresDialect.js +167 -0
  38. package/dist/query/postgresDialect.js.map +1 -0
  39. package/dist/query/relational.d.ts +69 -0
  40. package/dist/query/relational.d.ts.map +1 -0
  41. package/dist/query/relational.js +94 -0
  42. package/dist/query/relational.js.map +1 -0
  43. package/package.json +5 -4
@@ -43,7 +43,7 @@ export function buildListSessions(projectId, opts, d) {
43
43
  SELECT
44
44
  session_id,
45
45
  ${d.anyValue("visitor_id")} AS visitor_id,
46
- count() AS events,
46
+ count(*) AS events,
47
47
  min(ts) AS started_at,
48
48
  max(ts) AS ended_at
49
49
  FROM events
@@ -73,11 +73,11 @@ export function buildPointerHeatmap(projectId, opts, d) {
73
73
  SELECT
74
74
  floor(screen[1] * ${bins}) AS gx,
75
75
  floor(screen[2] * ${bins}) AS gy,
76
- count() AS count
76
+ count(*) AS count
77
77
  FROM events
78
78
  WHERE project_id = ${pid}
79
79
  AND event_type IN ('pointer_move', 'pointer_click')
80
- AND length(screen) = 2${range}${scene}${source}${session}${cameraMode}
80
+ AND ${d.arrayLength("screen")} = 2${range}${scene}${source}${session}${cameraMode}
81
81
  GROUP BY gx, gy
82
82
  ORDER BY count DESC
83
83
  `,
@@ -109,7 +109,7 @@ export function buildMeshUvHeatmap(projectId, opts, d) {
109
109
  const v = d.jsonFloat("payload", "uv", "1");
110
110
  return {
111
111
  query: `
112
- SELECT gx, gy, count() AS count
112
+ SELECT gx, gy, count(*) AS count
113
113
  FROM (
114
114
  SELECT
115
115
  floor(${u} * ${bins}) AS gx,
@@ -147,11 +147,11 @@ export function buildWorldHeatmap(projectId, opts, d) {
147
147
  floor(hit_point[1] / ${cellSize}) AS vx,
148
148
  floor(hit_point[2] / ${cellSize}) AS vy,
149
149
  floor(hit_point[3] / ${cellSize}) AS vz,
150
- count() AS count
150
+ count(*) AS count
151
151
  FROM events
152
152
  WHERE project_id = ${pid}
153
153
  AND event_type IN ('pointer_move', 'pointer_click')
154
- AND length(hit_point) = 3${range}${scene}${source}${cameraMode}${region}
154
+ AND ${d.arrayLength("hit_point")} = 3${range}${scene}${source}${cameraMode}${region}
155
155
  GROUP BY vx, vy, vz
156
156
  ORDER BY count DESC
157
157
  LIMIT ${limit}
@@ -177,17 +177,17 @@ export function buildWorldHeatmapStats(projectId, opts, d) {
177
177
  const region = regionClause(bag, opts, HIT_POINT_COLS);
178
178
  return {
179
179
  query: `
180
- SELECT count() AS cells, coalesce(sum(c), 0) AS hits
180
+ SELECT count(*) AS cells, coalesce(sum(c), 0) AS hits
181
181
  FROM (
182
182
  SELECT
183
183
  floor(hit_point[1] / ${cellSize}) AS vx,
184
184
  floor(hit_point[2] / ${cellSize}) AS vy,
185
185
  floor(hit_point[3] / ${cellSize}) AS vz,
186
- count() AS c
186
+ count(*) AS c
187
187
  FROM events
188
188
  WHERE project_id = ${pid}
189
189
  AND event_type IN ('pointer_move', 'pointer_click')
190
- AND length(hit_point) = 3${range}${scene}${source}${cameraMode}${region}
190
+ AND ${d.arrayLength("hit_point")} = 3${range}${scene}${source}${cameraMode}${region}
191
191
  GROUP BY vx, vy, vz
192
192
  ) t
193
193
  `,
@@ -220,11 +220,11 @@ export function buildGazeHeatmap(projectId, opts, d) {
220
220
  floor(hit_point[1] / ${cellSize}) AS vx,
221
221
  floor(hit_point[2] / ${cellSize}) AS vy,
222
222
  floor(hit_point[3] / ${cellSize}) AS vz,
223
- count() AS count
223
+ count(*) AS count
224
224
  FROM events
225
225
  WHERE project_id = ${pid}
226
226
  AND event_type = 'camera_sample'
227
- AND length(hit_point) = 3${range}${scene}${session}${cameraMode}${region}
227
+ AND ${d.arrayLength("hit_point")} = 3${range}${scene}${session}${cameraMode}${region}
228
228
  GROUP BY vx, vy, vz
229
229
  ORDER BY count DESC
230
230
  LIMIT ${limit}
@@ -248,17 +248,17 @@ export function buildGazeHeatmapStats(projectId, opts, d) {
248
248
  const region = regionClause(bag, opts, HIT_POINT_COLS);
249
249
  return {
250
250
  query: `
251
- SELECT count() AS cells, coalesce(sum(c), 0) AS hits
251
+ SELECT count(*) AS cells, coalesce(sum(c), 0) AS hits
252
252
  FROM (
253
253
  SELECT
254
254
  floor(hit_point[1] / ${cellSize}) AS vx,
255
255
  floor(hit_point[2] / ${cellSize}) AS vy,
256
256
  floor(hit_point[3] / ${cellSize}) AS vz,
257
- count() AS c
257
+ count(*) AS c
258
258
  FROM events
259
259
  WHERE project_id = ${pid}
260
260
  AND event_type = 'camera_sample'
261
- AND length(hit_point) = 3${range}${scene}${session}${cameraMode}${region}
261
+ AND ${d.arrayLength("hit_point")} = 3${range}${scene}${session}${cameraMode}${region}
262
262
  GROUP BY vx, vy, vz
263
263
  ) t
264
264
  `,
@@ -282,11 +282,11 @@ export function buildCameraDirectionHeatmap(projectId, opts, d) {
282
282
  SELECT
283
283
  floor((atan2(direction[3], direction[1]) + pi()) / (2 * pi()) * ${bins}) AS azimuth_bin,
284
284
  floor((asin(direction[2] / greatest(${d.vectorNorm("direction")}, 1e-6)) + pi() / 2) / pi() * ${bins}) AS elevation_bin,
285
- count() AS count
285
+ count(*) AS count
286
286
  FROM events
287
287
  WHERE project_id = ${pid}
288
288
  AND event_type = 'camera_sample'
289
- AND length(direction) = 3${range}${scene}${session}${cameraMode}
289
+ AND ${d.arrayLength("direction")} = 3${range}${scene}${session}${cameraMode}
290
290
  GROUP BY azimuth_bin, elevation_bin
291
291
  ORDER BY count DESC
292
292
  `,
@@ -319,7 +319,7 @@ export function buildViewCoverageHistogram(projectId, opts, d) {
319
319
  query: `
320
320
  SELECT
321
321
  least(floor(coverage_pct / 25), 3) * 25 AS bucket,
322
- count() AS sessions
322
+ count(*) AS sessions
323
323
  FROM (
324
324
  SELECT
325
325
  session_id,
@@ -332,7 +332,7 @@ export function buildViewCoverageHistogram(projectId, opts, d) {
332
332
  FROM events
333
333
  WHERE project_id = ${pid}
334
334
  AND event_type = 'camera_sample'
335
- AND length(direction) = 3${range}${scene}${session}${cameraMode}
335
+ AND ${d.arrayLength("direction")} = 3${range}${scene}${session}${cameraMode}
336
336
  ) binned
337
337
  GROUP BY session_id
338
338
  ) per_session
@@ -364,11 +364,11 @@ export function buildCameraPositionHeatmap(projectId, opts, d) {
364
364
  floor(position[1] / ${cellSize}) AS gx,
365
365
  floor(position[3] / ${cellSize}) AS gz,
366
366
  avg(position[2]) AS avg_y,
367
- count() AS count
367
+ count(*) AS count
368
368
  FROM events
369
369
  WHERE project_id = ${pid}
370
370
  AND event_type = 'camera_sample'
371
- AND length(position) = 3${range}${scene}${session}${cameraMode}${region}
371
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}${cameraMode}${region}
372
372
  GROUP BY gx, gz
373
373
  ORDER BY count DESC
374
374
  LIMIT ${limit}
@@ -399,7 +399,7 @@ export function buildSessionTrajectory(projectId, opts, d) {
399
399
  WHERE project_id = ${pid}
400
400
  AND session_id = ${session}
401
401
  AND event_type = 'camera_sample'
402
- AND length(position) = 3${range}${scene}
402
+ AND ${d.arrayLength("position")} = 3${range}${scene}
403
403
  ORDER BY ts ASC
404
404
  LIMIT ${limit}
405
405
  `,
@@ -436,7 +436,7 @@ export function buildAggregateTrajectories(projectId, opts, d) {
436
436
  FROM events
437
437
  WHERE project_id = ${pid}
438
438
  AND event_type = 'camera_sample'
439
- AND length(position) = 3${range}${scene}${cameraMode}
439
+ AND ${d.arrayLength("position")} = 3${range}${scene}${cameraMode}
440
440
  ORDER BY session_id ASC, ts ASC
441
441
  LIMIT ${limit}
442
442
  `,
@@ -489,26 +489,26 @@ export function buildClickGazeRay(projectId, opts, d) {
489
489
  avg(j.hy) AS hit_y,
490
490
  avg(j.hz) AS hit_z,
491
491
  j.mesh AS mesh,
492
- count() AS count
492
+ count(*) AS count
493
493
  FROM (
494
494
  SELECT
495
495
  ja.hx AS hx, ja.hy AS hy, ja.hz AS hz, ja.mesh AS mesh,
496
496
  CASE
497
- WHEN ja.has_ray THEN ja.rox
498
- WHEN ja.recon THEN ja.px + ja.dx * ja.near / ja.dlen
497
+ WHEN ja.has_ray = 1 THEN ja.rox
498
+ WHEN ja.recon = 1 THEN ja.px + ja.dx * ja.near / ja.dlen
499
499
  + (ja.dz / ja.hlen) * ja.off_r
500
500
  + (-(ja.dx * ja.dy) / (ja.dlen * ja.hlen)) * ja.off_u
501
501
  WHEN ja.cam_present = 1 THEN ja.px
502
502
  END AS ox,
503
503
  CASE
504
- WHEN ja.has_ray THEN ja.roy
505
- WHEN ja.recon THEN ja.py + ja.dy * ja.near / ja.dlen
504
+ WHEN ja.has_ray = 1 THEN ja.roy
505
+ WHEN ja.recon = 1 THEN ja.py + ja.dy * ja.near / ja.dlen
506
506
  + (ja.hlen / ja.dlen) * ja.off_u
507
507
  WHEN ja.cam_present = 1 THEN ja.py
508
508
  END AS oy,
509
509
  CASE
510
- WHEN ja.has_ray THEN ja.roz
511
- WHEN ja.recon THEN ja.pz + ja.dz * ja.near / ja.dlen
510
+ WHEN ja.has_ray = 1 THEN ja.roz
511
+ WHEN ja.recon = 1 THEN ja.pz + ja.dz * ja.near / ja.dlen
512
512
  + (-(ja.dx) / ja.hlen) * ja.off_r
513
513
  + (-(ja.dy * ja.dz) / (ja.dlen * ja.hlen)) * ja.off_u
514
514
  WHEN ja.cam_present = 1 THEN ja.pz
@@ -524,23 +524,26 @@ export function buildClickGazeRay(projectId, opts, d) {
524
524
  sqrt(m.dx * m.dx + m.dz * m.dz) AS hlen,
525
525
  (2 * c.sx - 1) * m.cam_near * tan(m.cam_fov / 2) * m.cam_aspect AS off_r,
526
526
  (1 - 2 * c.sy) * m.cam_near * tan(m.cam_fov / 2) AS off_u,
527
- (NOT c.has_ray) AND c.has_screen
527
+ CASE WHEN c.has_ray = 0 AND c.has_screen = 1
528
528
  AND m.cam_fov > 0 AND m.cam_aspect > 0 AND m.cam_near > 0
529
529
  AND sqrt(m.dx * m.dx + m.dz * m.dz)
530
- > 1e-6 * sqrt(m.dx * m.dx + m.dy * m.dy + m.dz * m.dz) AS recon
530
+ > 1e-6 * sqrt(m.dx * m.dx + m.dy * m.dy + m.dz * m.dz)
531
+ THEN 1 ELSE 0 END AS recon
531
532
  FROM (
532
533
  SELECT session_id, ts,
533
534
  hit_point[1] AS hx, hit_point[2] AS hy, hit_point[3] AS hz, mesh,
534
- length(ray_origin) = 3 AS has_ray,
535
+ CASE WHEN ${d.arrayLength("ray_origin")} = 3 THEN 1 ELSE 0 END AS has_ray,
535
536
  ray_origin[1] AS rox, ray_origin[2] AS roy, ray_origin[3] AS roz,
536
- length(screen) = 2 AS has_screen,
537
+ CASE WHEN ${d.arrayLength("screen")} = 2 THEN 1 ELSE 0 END AS has_screen,
537
538
  screen[1] AS sx, screen[2] AS sy
538
539
  FROM events
539
540
  WHERE project_id = ${pid}
540
541
  AND event_type = 'pointer_click'
541
- AND length(hit_point) = 3${range}${scene}${source}${session}
542
+ AND ${d.arrayLength("hit_point")} = 3${range}${scene}${source}${session}
542
543
  ) AS c
543
- ${d.asofLeftJoin} (
544
+ ${d.asofJoin({
545
+ kind: "left",
546
+ right: `(
544
547
  SELECT session_id, ts, 1 AS cam_present,
545
548
  position[1] AS px, position[2] AS py, position[3] AS pz,
546
549
  direction[1] AS dx, direction[2] AS dy, direction[3] AS dz,
@@ -548,9 +551,14 @@ export function buildClickGazeRay(projectId, opts, d) {
548
551
  FROM events
549
552
  WHERE project_id = ${pid}
550
553
  AND event_type = 'camera_sample'
551
- AND length(position) = 3${range}${scene}${session}
552
- ) AS m
553
- ON c.session_id = m.session_id AND c.ts >= m.ts
554
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}
555
+ )`,
556
+ alias: "m",
557
+ keys: [["c.session_id", "session_id"]],
558
+ leftTs: "c.ts",
559
+ op: ">=",
560
+ rightTs: "ts",
561
+ })}
554
562
  ) AS ja
555
563
  ) AS j
556
564
  WHERE j.ox IS NOT NULL
@@ -594,7 +602,7 @@ export function buildFlowHeatmap(projectId, opts, d) {
594
602
  floor((atan2(m.dz, m.dx) + pi()) / (2 * pi()) * ${bins}) AS azimuth_bin,
595
603
  floor((asin(m.dy / greatest(sqrt(m.dx * m.dx + m.dy * m.dy + m.dz * m.dz), 1e-6)) + pi() / 2) / pi() * ${bins}) AS elevation_bin,
596
604
  c.mesh AS mesh,
597
- count() AS count
605
+ count(*) AS count
598
606
  FROM (
599
607
  SELECT session_id, ts, mesh
600
608
  FROM events
@@ -602,14 +610,21 @@ export function buildFlowHeatmap(projectId, opts, d) {
602
610
  AND event_type = 'pointer_click'
603
611
  AND mesh != ''${range}${scene}${session}${cameraMode}
604
612
  ) AS c
605
- ${d.asofInnerJoin} (
613
+ ${d.asofJoin({
614
+ kind: "inner",
615
+ right: `(
606
616
  SELECT session_id, ts, direction[1] AS dx, direction[2] AS dy, direction[3] AS dz
607
617
  FROM events
608
618
  WHERE project_id = ${pid}
609
619
  AND event_type = 'camera_sample'
610
- AND length(direction) = 3${range}${scene}${session}${cameraMode}
611
- ) AS m
612
- ON c.session_id = m.session_id AND c.ts >= m.ts
620
+ AND ${d.arrayLength("direction")} = 3${range}${scene}${session}${cameraMode}
621
+ )`,
622
+ alias: "m",
623
+ keys: [["c.session_id", "session_id"]],
624
+ leftTs: "c.ts",
625
+ op: ">=",
626
+ rightTs: "ts",
627
+ })}
613
628
  GROUP BY azimuth_bin, elevation_bin, mesh
614
629
  ORDER BY count DESC
615
630
  LIMIT ${limit}
@@ -640,34 +655,41 @@ export function buildFlowHeatmap(projectId, opts, d) {
640
655
  avg(j.oy) AS origin_y,
641
656
  avg(j.oz) AS origin_z,
642
657
  j.mesh AS mesh,
643
- count() AS count
658
+ count(*) AS count
644
659
  FROM (
645
660
  SELECT
646
661
  c.mesh AS mesh,
647
662
  m.dx AS dx, m.dy AS dy, m.dz AS dz,
648
- CASE WHEN c.has_ray THEN c.rox ELSE m.px END AS ox,
649
- CASE WHEN c.has_ray THEN c.roy ELSE m.py END AS oy,
650
- CASE WHEN c.has_ray THEN c.roz ELSE m.pz END AS oz
663
+ CASE WHEN c.has_ray = 1 THEN c.rox ELSE m.px END AS ox,
664
+ CASE WHEN c.has_ray = 1 THEN c.roy ELSE m.py END AS oy,
665
+ CASE WHEN c.has_ray = 1 THEN c.roz ELSE m.pz END AS oz
651
666
  FROM (
652
667
  SELECT session_id, ts, mesh,
653
- length(ray_origin) = 3 AS has_ray,
668
+ CASE WHEN ${d.arrayLength("ray_origin")} = 3 THEN 1 ELSE 0 END AS has_ray,
654
669
  ray_origin[1] AS rox, ray_origin[2] AS roy, ray_origin[3] AS roz
655
670
  FROM events
656
671
  WHERE project_id = ${pid}
657
672
  AND event_type = 'pointer_click'
658
673
  AND mesh != ''${range}${scene}${session}${cameraMode}
659
674
  ) AS c
660
- ${d.asofInnerJoin} (
675
+ ${d.asofJoin({
676
+ kind: "inner",
677
+ right: `(
661
678
  SELECT session_id, ts,
662
679
  position[1] AS px, position[2] AS py, position[3] AS pz,
663
680
  direction[1] AS dx, direction[2] AS dy, direction[3] AS dz
664
681
  FROM events
665
682
  WHERE project_id = ${pid}
666
683
  AND event_type = 'camera_sample'
667
- AND length(direction) = 3
668
- AND length(position) = 3${range}${scene}${session}${cameraMode}
669
- ) AS m
670
- ON c.session_id = m.session_id AND c.ts >= m.ts
684
+ AND ${d.arrayLength("direction")} = 3
685
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}${cameraMode}
686
+ )`,
687
+ alias: "m",
688
+ keys: [["c.session_id", "session_id"]],
689
+ leftTs: "c.ts",
690
+ op: ">=",
691
+ rightTs: "ts",
692
+ })}
671
693
  ) AS j${originFilter}
672
694
  GROUP BY azimuth_bin, elevation_bin, origin_vx, origin_vy, origin_vz, mesh
673
695
  ORDER BY count DESC
@@ -685,7 +707,7 @@ export function buildTopMeshes(projectId, opts, d) {
685
707
  const limit = bag.add("limit", "u32", opts.limit ?? 25);
686
708
  return {
687
709
  query: `
688
- SELECT mesh, count() AS count
710
+ SELECT mesh, count(*) AS count
689
711
  FROM events
690
712
  WHERE project_id = ${pid} AND mesh != ''${range}${session}
691
713
  GROUP BY mesh
@@ -714,7 +736,7 @@ export function buildTopMeshesBySource(projectId, opts, d) {
714
736
  const limit = bag.add("limit", "u32", opts.limit ?? 200);
715
737
  return {
716
738
  query: `
717
- SELECT mesh, source, count() AS count
739
+ SELECT mesh, source, count(*) AS count
718
740
  FROM events
719
741
  WHERE project_id = ${pid}
720
742
  AND event_type IN ('mesh_interaction', 'pointer_click')
@@ -749,7 +771,7 @@ export function buildTopMeshesTrend(projectId, opts, d) {
749
771
  SELECT
750
772
  mesh,
751
773
  ${d.timeBucketMs("ts", interval)} AS bucket,
752
- count() AS count
774
+ count(*) AS count
753
775
  FROM events
754
776
  WHERE project_id = ${pid}
755
777
  AND event_type IN ('mesh_interaction', 'pointer_click')
@@ -780,7 +802,7 @@ export function buildMeshDwell(projectId, opts, d) {
780
802
  sum(visible_ms) AS visible_ms,
781
803
  sum(centered_ms) AS centered_ms,
782
804
  max(screen_fraction) AS max_screen_fraction,
783
- count() AS samples
805
+ count(*) AS samples
784
806
  FROM events
785
807
  WHERE project_id = ${pid} AND event_type = 'mesh_visibility' AND mesh != ''${range}${scene}${session}
786
808
  GROUP BY mesh
@@ -865,7 +887,7 @@ export function buildMeshInteractionKinds(projectId, opts, d) {
865
887
  SELECT
866
888
  mesh,
867
889
  name AS kind,
868
- count() AS count
890
+ count(*) AS count
869
891
  FROM events
870
892
  WHERE project_id = ${pid} AND event_type = 'mesh_interaction' AND mesh != ''${range}${scene}${source}${session}
871
893
  GROUP BY mesh, name
@@ -906,7 +928,7 @@ export function buildReachability(projectId, opts, d) {
906
928
  SELECT
907
929
  seg.mesh AS mesh,
908
930
  floor(seg.dist / ${bucketSize}) AS bucket,
909
- count() AS count,
931
+ count(*) AS count,
910
932
  avg(seg.dist) AS avg_distance
911
933
  FROM (
912
934
  SELECT
@@ -923,17 +945,24 @@ export function buildReachability(projectId, opts, d) {
923
945
  WHERE project_id = ${pid}
924
946
  AND event_type = 'mesh_interaction'
925
947
  AND mesh != ''
926
- AND length(hit_point) = 3${range}${scene}${source}${session}
948
+ AND ${d.arrayLength("hit_point")} = 3${range}${scene}${source}${session}
927
949
  ) AS i
928
- ${d.asofInnerJoin} (
950
+ ${d.asofJoin({
951
+ kind: "inner",
952
+ right: `(
929
953
  SELECT session_id, ts,
930
954
  position[1] AS px, position[2] AS py, position[3] AS pz
931
955
  FROM events
932
956
  WHERE project_id = ${pid}
933
957
  AND event_type = 'camera_sample'
934
- AND length(position) = 3${range}${scene}${session}
935
- ) AS m
936
- ON i.session_id = m.session_id AND i.ts >= m.ts
958
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}
959
+ )`,
960
+ alias: "m",
961
+ keys: [["i.session_id", "session_id"]],
962
+ leftTs: "i.ts",
963
+ op: ">=",
964
+ rightTs: "ts",
965
+ })}
937
966
  ) AS seg
938
967
  GROUP BY mesh, bucket
939
968
  ORDER BY count DESC
@@ -958,7 +987,7 @@ export function buildDeadClicks(projectId, opts, d) {
958
987
  return {
959
988
  query: `
960
989
  SELECT
961
- count() AS total_clicks,
990
+ count(*) AS total_clicks,
962
991
  sum(CASE WHEN mesh = '' THEN 1 ELSE 0 END) AS dead_clicks
963
992
  FROM events
964
993
  WHERE project_id = ${pid} AND event_type = 'pointer_click'${range}${scene}${source}${session}
@@ -991,11 +1020,11 @@ export function buildRageClicks(projectId, opts, d) {
991
1020
  session_id,
992
1021
  mesh,
993
1022
  ${d.timeBucketMs("ts", interval)} AS bucket,
994
- count() AS clicks
1023
+ count(*) AS clicks
995
1024
  FROM events
996
1025
  WHERE project_id = ${pid} AND event_type = 'pointer_click' AND mesh != ''${range}${scene}${source}${session}
997
1026
  GROUP BY session_id, mesh, bucket
998
- HAVING count() >= ${minRepeats}
1027
+ HAVING count(*) >= ${minRepeats}
999
1028
  ORDER BY clicks DESC
1000
1029
  LIMIT ${limit}
1001
1030
  `,
@@ -1024,7 +1053,7 @@ export function buildHoverDwell(projectId, opts, d) {
1024
1053
  mesh,
1025
1054
  sum(visible_ms) AS dwell_ms,
1026
1055
  max(visible_ms) AS max_dwell_ms,
1027
- count() AS episodes
1056
+ count(*) AS episodes
1028
1057
  FROM events
1029
1058
  WHERE project_id = ${pid} AND event_type = 'hover_dwell' AND mesh != ''${range}${scene}${source}${session}
1030
1059
  GROUP BY mesh
@@ -1053,7 +1082,7 @@ export function buildCompileStalls(projectId, opts, d) {
1053
1082
  query: `
1054
1083
  SELECT
1055
1084
  name AS phase,
1056
- count() AS stalls,
1085
+ count(*) AS stalls,
1057
1086
  sum(visible_ms) AS total_ms,
1058
1087
  avg(visible_ms) AS avg_ms,
1059
1088
  max(visible_ms) AS max_ms
@@ -1081,7 +1110,7 @@ export function buildResourceSummary(projectId, opts, d) {
1081
1110
  return {
1082
1111
  query: `
1083
1112
  SELECT
1084
- count() AS samples,
1113
+ count(*) AS samples,
1085
1114
  avg(NULLIF(js_heap_bytes, 0)) AS avg_js_heap_bytes,
1086
1115
  max(js_heap_bytes) AS max_js_heap_bytes,
1087
1116
  avg(NULLIF(triangles, 0)) AS avg_triangles,
@@ -1116,7 +1145,7 @@ export function buildResourcePercentiles(projectId, opts, d) {
1116
1145
  return {
1117
1146
  query: `
1118
1147
  SELECT
1119
- count() AS sessions,
1148
+ count(*) AS sessions,
1120
1149
  sum(s_samples) AS samples,
1121
1150
  ${d.quantile("s_heap_p50", 0.5)} AS p50_js_heap_bytes,
1122
1151
  ${d.quantile("s_heap_p95", 0.5)} AS p95_js_heap_bytes,
@@ -1127,7 +1156,7 @@ export function buildResourcePercentiles(projectId, opts, d) {
1127
1156
  FROM (
1128
1157
  SELECT
1129
1158
  session_id,
1130
- count() AS s_samples,
1159
+ count(*) AS s_samples,
1131
1160
  ${d.quantile("nullIf(js_heap_bytes, 0)", 0.5)} AS s_heap_p50,
1132
1161
  ${d.quantile("nullIf(js_heap_bytes, 0)", 0.95)} AS s_heap_p95,
1133
1162
  ${d.quantile("nullIf(texture_bytes, 0)", 0.5)} AS s_tex_p50,
@@ -1160,7 +1189,7 @@ export function buildStabilityCounts(projectId, opts, d) {
1160
1189
  SELECT
1161
1190
  coalesce(sum(CASE WHEN event_type = 'context_lost' THEN 1 ELSE 0 END), 0) AS context_losses,
1162
1191
  coalesce(sum(CASE WHEN event_type = 'compile_stall' THEN 1 ELSE 0 END), 0) AS compile_stalls,
1163
- count() AS incidents
1192
+ count(*) AS incidents
1164
1193
  FROM events
1165
1194
  WHERE project_id = ${pid} AND event_type IN ('context_lost', 'compile_stall')${range}${scene}${session}
1166
1195
  `,
@@ -1218,7 +1247,7 @@ export function buildGraphicsDiagnosticCounts(projectId, opts, d) {
1218
1247
  *
1219
1248
  * `position` is best-effort connector-side (the camera pose at the moment the
1220
1249
  * error/diagnostic fired), so only rows that carry a full 3-vector participate
1221
- * (`length(position) = 3`); errors from pages with no 3D connector are naturally
1250
+ * (`arrayLength(position) = 3`); errors from pages with no 3D connector are naturally
1222
1251
  * excluded. Reuses the already-promoted `position` column — no migration.
1223
1252
  *
1224
1253
  * Optional {@link ErrorHeatmapOptions} filters (severity/category/errorKind) read
@@ -1253,11 +1282,11 @@ export function buildErrorHeatmap(projectId, opts, d) {
1253
1282
  floor(position[1] / ${cellSize}) AS vx,
1254
1283
  floor(position[2] / ${cellSize}) AS vy,
1255
1284
  floor(position[3] / ${cellSize}) AS vz,
1256
- count() AS count
1285
+ count(*) AS count
1257
1286
  FROM events
1258
1287
  WHERE project_id = ${pid}
1259
1288
  AND event_type IN ('runtime_error', 'graphics_diagnostic')
1260
- AND length(position) = 3${range}${scene}${session}${region}${filter}
1289
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}${region}${filter}
1261
1290
  GROUP BY vx, vy, vz
1262
1291
  ORDER BY count DESC
1263
1292
  LIMIT ${limit}
@@ -1277,7 +1306,7 @@ export function buildErrorHeatmap(projectId, opts, d) {
1277
1306
  * in the already-promoted `position` column (no migration), and the WebXR
1278
1307
  * bounds-check that produced it ran entirely on-device — the boundary polygon and
1279
1308
  * room geometry are never captured or stored (ADR 0003 / ADR 0048). Only rows
1280
- * with a full 3-vector participate (`length(position) = 3`). `region` drill-down
1309
+ * with a full 3-vector participate (`arrayLength(position) = 3`). `region` drill-down
1281
1310
  * and `scene`/`session` scoping compose like the other spatial heatmaps.
1282
1311
  */
1283
1312
  export function buildBoundaryHeatmap(projectId, opts, d) {
@@ -1295,11 +1324,11 @@ export function buildBoundaryHeatmap(projectId, opts, d) {
1295
1324
  floor(position[1] / ${cellSize}) AS vx,
1296
1325
  floor(position[2] / ${cellSize}) AS vy,
1297
1326
  floor(position[3] / ${cellSize}) AS vz,
1298
- count() AS count
1327
+ count(*) AS count
1299
1328
  FROM events
1300
1329
  WHERE project_id = ${pid}
1301
1330
  AND event_type = 'xr_boundary_proximity'
1302
- AND length(position) = 3${range}${scene}${session}${region}
1331
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}${region}
1303
1332
  GROUP BY vx, vy, vz
1304
1333
  ORDER BY count DESC
1305
1334
  LIMIT ${limit}
@@ -1324,17 +1353,17 @@ export function buildBoundaryHeatmapStats(projectId, opts, d) {
1324
1353
  const region = regionClause(bag, opts, POSITION_COLS);
1325
1354
  return {
1326
1355
  query: `
1327
- SELECT count() AS cells, coalesce(sum(c), 0) AS hits
1356
+ SELECT count(*) AS cells, coalesce(sum(c), 0) AS hits
1328
1357
  FROM (
1329
1358
  SELECT
1330
1359
  floor(position[1] / ${cellSize}) AS vx,
1331
1360
  floor(position[2] / ${cellSize}) AS vy,
1332
1361
  floor(position[3] / ${cellSize}) AS vz,
1333
- count() AS c
1362
+ count(*) AS c
1334
1363
  FROM events
1335
1364
  WHERE project_id = ${pid}
1336
1365
  AND event_type = 'xr_boundary_proximity'
1337
- AND length(position) = 3${range}${scene}${session}${region}
1366
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}${region}
1338
1367
  GROUP BY vx, vy, vz
1339
1368
  ) t
1340
1369
  `,
@@ -1349,7 +1378,7 @@ export function buildBoundaryHeatmapStats(projectId, opts, d) {
1349
1378
  * space didn't fit the experience.
1350
1379
  *
1351
1380
  * Each `xr_boundary_proximity` event is one approach (ADR 0048), so `contacts` is
1352
- * a plain `count()` and `near_ms` sums the per-approach `durationMs` (stored in
1381
+ * a plain `count(*)` and `near_ms` sums the per-approach `durationMs` (stored in
1353
1382
  * the shared `visible_ms` column, reused by `mesh_visibility`/`hover_dwell`/
1354
1383
  * `compile_stall`). No boundary geometry participates — only the promoted position
1355
1384
  * + duration each event already carries.
@@ -1365,7 +1394,7 @@ export function buildBoundaryContacts(projectId, opts, d) {
1365
1394
  query: `
1366
1395
  SELECT
1367
1396
  session_id,
1368
- count() AS contacts,
1397
+ count(*) AS contacts,
1369
1398
  coalesce(sum(visible_ms), 0) AS near_ms
1370
1399
  FROM events
1371
1400
  WHERE project_id = ${pid}
@@ -1406,7 +1435,7 @@ export function buildRenderingTechnology(projectId, opts, d) {
1406
1435
  coalesce(${backend}, '') AS backend,
1407
1436
  coalesce(${apiVersion}, '') AS api_version,
1408
1437
  coalesce(${shadingLanguage}, '') AS shading_language,
1409
- count() AS sessions
1438
+ count(*) AS sessions
1410
1439
  FROM events
1411
1440
  WHERE project_id = ${pid} AND event_type = 'session_start'${range}${scene}${session}
1412
1441
  GROUP BY api, backend, api_version, shading_language
@@ -1435,7 +1464,7 @@ export function buildCapabilityChanges(projectId, opts, d) {
1435
1464
  name AS kind,
1436
1465
  cap_from AS "from",
1437
1466
  cap_to AS "to",
1438
- count() AS changes
1467
+ count(*) AS changes
1439
1468
  FROM events
1440
1469
  WHERE project_id = ${pid} AND event_type = 'capability_change'${range}${scene}${session}
1441
1470
  GROUP BY name, cap_from, cap_to
@@ -1466,7 +1495,7 @@ export function buildCameraGestures(projectId, opts, d) {
1466
1495
  query: `
1467
1496
  SELECT
1468
1497
  name AS kind,
1469
- count() AS gestures,
1498
+ count(*) AS gestures,
1470
1499
  sum(visible_ms) AS total_ms,
1471
1500
  avg(visible_ms) AS avg_ms,
1472
1501
  max(visible_ms) AS max_ms
@@ -1488,7 +1517,7 @@ export function buildPerfSummary(projectId, opts, d) {
1488
1517
  return {
1489
1518
  query: `
1490
1519
  SELECT
1491
- count() AS samples,
1520
+ count(*) AS samples,
1492
1521
  avg(fps) AS avg_fps,
1493
1522
  min(fps) AS min_fps,
1494
1523
  ${d.quantile("fps", 0.5)} AS p50_fps
@@ -1519,7 +1548,7 @@ export function buildRenderScaleTruth(projectId, opts, d) {
1519
1548
  return {
1520
1549
  query: `
1521
1550
  SELECT
1522
- count() AS samples,
1551
+ count(*) AS samples,
1523
1552
  avg(fps) AS avg_fps,
1524
1553
  ${d.quantile("fps", 0.5)} AS p50_fps,
1525
1554
  avg(NULLIF(render_scale, 0)) AS avg_render_scale,
@@ -1549,7 +1578,7 @@ export function buildPerfDistribution(projectId, opts, d) {
1549
1578
  return {
1550
1579
  query: `
1551
1580
  SELECT
1552
- count() AS sessions,
1581
+ count(*) AS sessions,
1553
1582
  sum(s_samples) AS samples,
1554
1583
  ${d.quantile("s_p05", 0.5)} AS p05_fps,
1555
1584
  ${d.quantile("s_p50", 0.5)} AS p50_fps,
@@ -1557,7 +1586,7 @@ export function buildPerfDistribution(projectId, opts, d) {
1557
1586
  FROM (
1558
1587
  SELECT
1559
1588
  session_id,
1560
- count() AS s_samples,
1589
+ count(*) AS s_samples,
1561
1590
  ${d.quantile("fps", 0.05)} AS s_p05,
1562
1591
  ${d.quantile("fps", 0.5)} AS s_p50,
1563
1592
  ${d.quantile("fps", 0.95)} AS s_p95
@@ -1588,7 +1617,7 @@ export function buildFpsHistogram(projectId, opts, d) {
1588
1617
  query: `
1589
1618
  SELECT
1590
1619
  floor(s_p50 / ${bucket}) * ${bucket} AS bucket,
1591
- count() AS sessions
1620
+ count(*) AS sessions
1592
1621
  FROM (
1593
1622
  SELECT
1594
1623
  session_id,
@@ -1621,14 +1650,14 @@ export function buildFrameTimePercentiles(projectId, opts, d) {
1621
1650
  return {
1622
1651
  query: `
1623
1652
  SELECT
1624
- count() AS sessions,
1653
+ count(*) AS sessions,
1625
1654
  sum(s_samples) AS samples,
1626
1655
  ${d.quantile("s_p50", 0.5)} AS p50_ms,
1627
1656
  ${d.quantile("s_p95", 0.5)} AS p95_ms
1628
1657
  FROM (
1629
1658
  SELECT
1630
1659
  session_id,
1631
- count() AS s_samples,
1660
+ count(*) AS s_samples,
1632
1661
  ${d.quantile("nullIf(frame_time_ms, 0)", 0.5)} AS s_p50,
1633
1662
  max(nullIf(frame_time_p95_ms, 0)) AS s_p95
1634
1663
  FROM events
@@ -1656,7 +1685,7 @@ export function buildJankRate(projectId, opts, d) {
1656
1685
  return {
1657
1686
  query: `
1658
1687
  SELECT
1659
- count() AS sessions,
1688
+ count(*) AS sessions,
1660
1689
  sum(s_long) AS total_long_frames,
1661
1690
  ${d.quantile("s_rate", 0.5)} AS median_rate,
1662
1691
  ${d.quantile("s_rate", 0.9)} AS worst_decile_rate
@@ -1664,7 +1693,7 @@ export function buildJankRate(projectId, opts, d) {
1664
1693
  SELECT
1665
1694
  session_id,
1666
1695
  sum(long_frames) AS s_long,
1667
- sum(long_frames) * 1.0 / count() AS s_rate
1696
+ sum(long_frames) * 1.0 / count(*) AS s_rate
1668
1697
  FROM events
1669
1698
  WHERE project_id = ${pid} AND event_type = 'frame_perf'${range}${scene}${session}
1670
1699
  GROUP BY session_id
@@ -1740,8 +1769,8 @@ export function buildPerfChurn(projectId, opts, d) {
1740
1769
  GROUP BY ends.session_id
1741
1770
  )
1742
1771
  SELECT
1743
- (SELECT count() FROM ends) AS sessions,
1744
- count() AS churn_sessions,
1772
+ (SELECT count(*) FROM ends) AS sessions,
1773
+ count(*) AS churn_sessions,
1745
1774
  sum(had_fps) AS fps_churn_sessions,
1746
1775
  sum(had_stall) AS stall_churn_sessions
1747
1776
  FROM correlated
@@ -1787,7 +1816,7 @@ export function buildPerfByDevice(projectId, opts, d) {
1787
1816
  session_perf AS (
1788
1817
  SELECT
1789
1818
  session_id,
1790
- count() AS s_samples,
1819
+ count(*) AS s_samples,
1791
1820
  ${d.quantile("fps", 0.5)} AS s_p50
1792
1821
  FROM events
1793
1822
  WHERE project_id = ${pid} AND event_type = 'frame_perf'${range}${scene}${session}
@@ -1799,7 +1828,7 @@ export function buildPerfByDevice(projectId, opts, d) {
1799
1828
  coalesce(dev.renderer, '') AS renderer,
1800
1829
  coalesce(dev.browser, '') AS browser,
1801
1830
  coalesce(dev.os, '') AS os,
1802
- count() AS sessions,
1831
+ count(*) AS sessions,
1803
1832
  sum(perf.s_samples) AS samples,
1804
1833
  ${d.quantile("perf.s_p50", 0.5)} AS p50_fps
1805
1834
  FROM session_perf perf
@@ -1827,14 +1856,14 @@ export function buildPerfByScene(projectId, opts, d) {
1827
1856
  query: `
1828
1857
  SELECT
1829
1858
  scene_id,
1830
- count() AS sessions,
1859
+ count(*) AS sessions,
1831
1860
  sum(s_samples) AS samples,
1832
1861
  ${d.quantile("s_p50", 0.5)} AS p50_fps
1833
1862
  FROM (
1834
1863
  SELECT
1835
1864
  session_id,
1836
1865
  ${d.anyValue("scene_id")} AS scene_id,
1837
- count() AS s_samples,
1866
+ count(*) AS s_samples,
1838
1867
  ${d.quantile("fps", 0.5)} AS s_p50
1839
1868
  FROM events
1840
1869
  WHERE project_id = ${pid} AND event_type = 'frame_perf'${range}${scene}${session}
@@ -1909,7 +1938,7 @@ export function buildDistinctScenes(projectId, opts, d) {
1909
1938
  query: `
1910
1939
  SELECT
1911
1940
  scene_id,
1912
- count() AS events,
1941
+ count(*) AS events,
1913
1942
  max(ts) AS last_seen
1914
1943
  FROM events
1915
1944
  WHERE project_id = ${pid}${range}
@@ -1938,7 +1967,7 @@ export function buildTimeseries(projectId, opts, d) {
1938
1967
  query: `
1939
1968
  SELECT
1940
1969
  ${d.timeBucketMs("ts", interval)} AS bucket,
1941
- count() AS events,
1970
+ count(*) AS events,
1942
1971
  ${d.avgIf("fps", "event_type = 'frame_perf'")} AS avg_fps
1943
1972
  FROM events
1944
1973
  WHERE project_id = ${pid}${range}${scene}${type}
@@ -1959,7 +1988,7 @@ export function buildEventTypeCounts(projectId, opts, d) {
1959
1988
  const scene = sceneClause(bag, opts);
1960
1989
  return {
1961
1990
  query: `
1962
- SELECT event_type, count() AS count
1991
+ SELECT event_type, count(*) AS count
1963
1992
  FROM events
1964
1993
  WHERE project_id = ${pid}${range}${scene}
1965
1994
  GROUP BY event_type
@@ -1990,11 +2019,11 @@ export function buildSceneCoverage(projectId, opts, d) {
1990
2019
  floor(position[1] / ${cellSize}) AS vx,
1991
2020
  floor(position[2] / ${cellSize}) AS vy,
1992
2021
  floor(position[3] / ${cellSize}) AS vz,
1993
- count() AS count
2022
+ count(*) AS count
1994
2023
  FROM events
1995
2024
  WHERE project_id = ${pid}
1996
2025
  AND event_type = 'camera_sample'
1997
- AND length(position) = 3${range}${scene}${session}
2026
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}
1998
2027
  GROUP BY vx, vy, vz
1999
2028
  ORDER BY count DESC
2000
2029
  LIMIT ${limit}
@@ -2029,13 +2058,13 @@ export function buildPerfHeatmap(projectId, opts, d) {
2029
2058
  floor(position[1] / ${cellSize}) AS vx,
2030
2059
  floor(position[2] / ${cellSize}) AS vy,
2031
2060
  floor(position[3] / ${cellSize}) AS vz,
2032
- count() AS samples,
2061
+ count(*) AS samples,
2033
2062
  avg(fps) AS avg_fps,
2034
2063
  min(fps) AS min_fps
2035
2064
  FROM events
2036
2065
  WHERE project_id = ${pid}
2037
2066
  AND event_type = 'frame_perf'
2038
- AND length(position) = 3${range}${scene}${session}
2067
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}
2039
2068
  GROUP BY vx, vy, vz
2040
2069
  ORDER BY avg_fps ASC
2041
2070
  LIMIT ${limit}
@@ -2070,11 +2099,11 @@ export function buildCameraDistance(projectId, opts, d) {
2070
2099
  (position[2] - ${cy}) * (position[2] - ${cy}) +
2071
2100
  (position[3] - ${cz}) * (position[3] - ${cz})
2072
2101
  ) / ${bucketSize}) AS bucket,
2073
- count() AS count
2102
+ count(*) AS count
2074
2103
  FROM events
2075
2104
  WHERE project_id = ${pid}
2076
2105
  AND event_type = 'camera_sample'
2077
- AND length(position) = 3${range}${scene}${session}
2106
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}
2078
2107
  GROUP BY bucket
2079
2108
  ORDER BY bucket ASC
2080
2109
  LIMIT ${limit}
@@ -2103,12 +2132,12 @@ export function buildNavigationStats(projectId, opts, d) {
2103
2132
  FROM events
2104
2133
  WHERE project_id = ${pid}
2105
2134
  AND event_type = 'camera_sample'
2106
- AND length(position) = 3${range}${scene}${session}`;
2135
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}`;
2107
2136
  return {
2108
2137
  query: `
2109
2138
  SELECT
2110
2139
  session_id,
2111
- count() AS segments,
2140
+ count(*) AS segments,
2112
2141
  sum(dist) AS total_distance,
2113
2142
  sum(CASE WHEN dist >= ${moveThreshold} THEN 1 ELSE 0 END) AS active_segments,
2114
2143
  sum(CASE WHEN dist >= ${moveThreshold} THEN dist ELSE 0 END) AS active_distance
@@ -2122,9 +2151,16 @@ export function buildNavigationStats(projectId, opts, d) {
2122
2151
  ) AS dist
2123
2152
  FROM (${sampleSelect}
2124
2153
  ) AS c
2125
- ${d.asofInnerJoin} (${sampleSelect}
2126
- ) AS m
2127
- ON c.session_id = m.session_id AND c.ts > m.ts
2154
+ ${d.asofJoin({
2155
+ kind: "inner",
2156
+ right: `(${sampleSelect}
2157
+ )`,
2158
+ alias: "m",
2159
+ keys: [["c.session_id", "session_id"]],
2160
+ leftTs: "c.ts",
2161
+ op: ">",
2162
+ rightTs: "ts",
2163
+ })}
2128
2164
  ) AS seg
2129
2165
  GROUP BY session_id
2130
2166
  ORDER BY total_distance DESC
@@ -2153,7 +2189,7 @@ export function buildNavigationStats(projectId, opts, d) {
2153
2189
  * Per (session, scene): `revisits = entries − distinct_cells` — every entry into
2154
2190
  * a cell beyond its first is a re-entry. Pooled per scene, the leaderboard
2155
2191
  * reports `backtrack_ratio = Σ revisits / Σ entries` alongside the raw counts.
2156
- * Only plain `count()` and a dedup subquery are used (no multi-column
2192
+ * Only plain `count(*)` and a dedup subquery are used (no multi-column
2157
2193
  * `COUNT(DISTINCT …)`), so DuckDB and ClickHouse agree.
2158
2194
  */
2159
2195
  export function buildBacktrackRatio(projectId, opts, d) {
@@ -2171,24 +2207,31 @@ export function buildBacktrackRatio(projectId, opts, d) {
2171
2207
  FROM events
2172
2208
  WHERE project_id = ${pid}
2173
2209
  AND event_type = 'camera_sample'
2174
- AND length(position) = 3${range}${scene}${session}`;
2210
+ AND ${d.arrayLength("position")} = 3${range}${scene}${session}`;
2175
2211
  // Ordered cell entries (consecutive same-cell samples collapsed): a sample is
2176
2212
  // an entry when it has no predecessor or its cell changed vs. the predecessor.
2177
2213
  const entries = `
2178
2214
  SELECT c.session_id AS session_id, c.scene AS scene, c.gx AS gx, c.gz AS gz
2179
2215
  FROM (${sampleSelect}
2180
2216
  ) AS c
2181
- ${d.asofLeftJoin} (
2217
+ ${d.asofJoin({
2218
+ kind: "left",
2219
+ right: `(
2182
2220
  SELECT session_id, ts, 1 AS present, gx, gz FROM (${sampleSelect}
2183
2221
  ) AS s
2184
- ) AS m
2185
- ON c.session_id = m.session_id AND c.ts > m.ts
2222
+ )`,
2223
+ alias: "m",
2224
+ keys: [["c.session_id", "session_id"]],
2225
+ leftTs: "c.ts",
2226
+ op: ">",
2227
+ rightTs: "ts",
2228
+ })}
2186
2229
  WHERE m.present IS NULL OR m.present = 0 OR c.gx <> m.gx OR c.gz <> m.gz`;
2187
2230
  return {
2188
2231
  query: `
2189
2232
  SELECT
2190
2233
  scene,
2191
- count() AS sessions,
2234
+ count(*) AS sessions,
2192
2235
  sum(total_entries) AS entries,
2193
2236
  sum(revisits) AS revisits,
2194
2237
  CASE WHEN sum(total_entries) > 0
@@ -2197,12 +2240,12 @@ export function buildBacktrackRatio(projectId, opts, d) {
2197
2240
  SELECT
2198
2241
  e.session_id AS session_id,
2199
2242
  e.scene AS scene,
2200
- count() AS total_entries,
2201
- count() - dc.distinct_cells AS revisits
2243
+ count(*) AS total_entries,
2244
+ count(*) - dc.distinct_cells AS revisits
2202
2245
  FROM (${entries}
2203
2246
  ) AS e
2204
2247
  JOIN (
2205
- SELECT session_id, scene, count() AS distinct_cells
2248
+ SELECT session_id, scene, count(*) AS distinct_cells
2206
2249
  FROM (
2207
2250
  SELECT DISTINCT session_id, scene, gx, gz FROM (${entries}
2208
2251
  ) AS de
@@ -2249,7 +2292,7 @@ export function buildInteractionsBySource(projectId, opts, d) {
2249
2292
  SELECT
2250
2293
  event_type,
2251
2294
  source,
2252
- count() AS count,
2295
+ count(*) AS count,
2253
2296
  count(DISTINCT session_id) AS sessions
2254
2297
  FROM events
2255
2298
  WHERE project_id = ${pid}
@@ -2283,7 +2326,7 @@ export function buildTopInputActions(projectId, opts, d) {
2283
2326
  SELECT
2284
2327
  name AS action,
2285
2328
  source,
2286
- count() AS count
2329
+ count(*) AS count
2287
2330
  FROM events
2288
2331
  WHERE project_id = ${pid} AND event_type = 'input_action' AND name != ''${range}${scene}${source}${session}
2289
2332
  GROUP BY name, source
@@ -2316,12 +2359,12 @@ export function buildXrRotationRate(projectId, opts, d) {
2316
2359
  FROM events
2317
2360
  WHERE project_id = ${pid}
2318
2361
  AND event_type = 'camera_sample'
2319
- AND length(direction) = 3${range}${scene}${session}`;
2362
+ AND ${d.arrayLength("direction")} = 3${range}${scene}${session}`;
2320
2363
  return {
2321
2364
  query: `
2322
2365
  SELECT
2323
2366
  session_id,
2324
- count() AS samples,
2367
+ count(*) AS samples,
2325
2368
  avg(turn) AS avg_turn_rad,
2326
2369
  max(turn) AS max_turn_rad,
2327
2370
  sum(turn) AS total_turn_rad,
@@ -2336,9 +2379,16 @@ export function buildXrRotationRate(projectId, opts, d) {
2336
2379
  ))) AS turn
2337
2380
  FROM (${sampleSelect}
2338
2381
  ) AS c
2339
- ${d.asofInnerJoin} (${sampleSelect}
2340
- ) AS m
2341
- ON c.session_id = m.session_id AND c.ts > m.ts
2382
+ ${d.asofJoin({
2383
+ kind: "inner",
2384
+ right: `(${sampleSelect}
2385
+ )`,
2386
+ alias: "m",
2387
+ keys: [["c.session_id", "session_id"]],
2388
+ leftTs: "c.ts",
2389
+ op: ">",
2390
+ rightTs: "ts",
2391
+ })}
2342
2392
  ) AS seg
2343
2393
  GROUP BY session_id
2344
2394
  ORDER BY total_turn_rad DESC
@@ -2365,7 +2415,7 @@ export function buildXrSourceUsage(projectId, opts, d) {
2365
2415
  query: `
2366
2416
  SELECT
2367
2417
  source,
2368
- count() AS interactions,
2418
+ count(*) AS interactions,
2369
2419
  count(DISTINCT session_id) AS sessions
2370
2420
  FROM events
2371
2421
  WHERE project_id = ${pid}
@@ -2396,7 +2446,7 @@ export function buildXrAbandonment(projectId, opts, d) {
2396
2446
  query: `
2397
2447
  SELECT
2398
2448
  session_id,
2399
- count() AS events,
2449
+ count(*) AS events,
2400
2450
  sum(CASE WHEN source IN ${XR_SOURCES} THEN 1 ELSE 0 END) AS xr_interactions,
2401
2451
  min(ts) AS started_at,
2402
2452
  max(ts) AS ended_at
@@ -2580,7 +2630,7 @@ export function buildArPlacementTimeToPlace(projectId, opts, d) {
2580
2630
  query: `
2581
2631
  SELECT
2582
2632
  floor(coalesce(${timeToPlace}, 0) / ${bucket}) * ${bucket} AS bucket,
2583
- count() AS placements
2633
+ count(*) AS placements
2584
2634
  FROM events
2585
2635
  WHERE project_id = ${pid} AND event_type = 'ar_placement'${range}${scene}${session}
2586
2636
  GROUP BY bucket
@@ -2610,7 +2660,7 @@ export function buildArPlacementAttempts(projectId, opts, d) {
2610
2660
  query: `
2611
2661
  SELECT
2612
2662
  coalesce(${attempts}, 1) AS attempts,
2613
- count() AS placements
2663
+ count(*) AS placements
2614
2664
  FROM events
2615
2665
  WHERE project_id = ${pid} AND event_type = 'ar_placement'${range}${scene}${session}
2616
2666
  GROUP BY attempts
@@ -2642,7 +2692,7 @@ export function buildArPlacementSurfaces(projectId, opts, d) {
2642
2692
  query: `
2643
2693
  SELECT
2644
2694
  coalesce(${surface}, 'unknown') AS surface,
2645
- count() AS placements,
2695
+ count(*) AS placements,
2646
2696
  avg(${scale}) AS avg_scale
2647
2697
  FROM events
2648
2698
  WHERE project_id = ${pid} AND event_type = 'ar_placement'${range}${scene}${session}
@@ -2683,7 +2733,7 @@ export function buildFunnel(projectId, opts, d) {
2683
2733
  )`;
2684
2734
  });
2685
2735
  const counts = steps
2686
- .map((_step, i) => `SELECT ${i} AS step, count() AS sessions FROM s${i}`)
2736
+ .map((_step, i) => `SELECT ${i} AS step, count(*) AS sessions FROM s${i}`)
2687
2737
  .join("\n UNION ALL ");
2688
2738
  return {
2689
2739
  query: `
@@ -2735,8 +2785,15 @@ export function buildSceneRetention(projectId, opts, d) {
2735
2785
  SELECT a.scene_id AS from_scene, b.scene_id AS to_scene,
2736
2786
  count(DISTINCT a.session_id) AS sessions
2737
2787
  FROM sc AS a
2738
- ${d.asofInnerJoin} sc AS b
2739
- ON a.session_id = b.session_id AND a.ts < b.ts
2788
+ ${d.asofJoin({
2789
+ kind: "inner",
2790
+ right: "sc",
2791
+ alias: "b",
2792
+ keys: [["a.session_id", "session_id"]],
2793
+ leftTs: "a.ts",
2794
+ op: "<",
2795
+ rightTs: "ts",
2796
+ })}
2740
2797
  GROUP BY from_scene, to_scene
2741
2798
  ORDER BY sessions DESC, from_scene ASC, to_scene ASC
2742
2799
  LIMIT ${limit}
@@ -2801,7 +2858,7 @@ export function buildLoadBounceFunnel(projectId, opts, d) {
2801
2858
  GROUP BY fl.session_id, fl.load_ts
2802
2859
  ),
2803
2860
  engaged AS (
2804
- SELECT lm.session_id AS session_id, count() AS interactions
2861
+ SELECT lm.session_id AS session_id, count(*) AS interactions
2805
2862
  FROM events AS e JOIN load_ms AS lm
2806
2863
  ON e.session_id = lm.session_id
2807
2864
  WHERE e.project_id = ${pid}
@@ -2811,7 +2868,7 @@ export function buildLoadBounceFunnel(projectId, opts, d) {
2811
2868
  )
2812
2869
  SELECT
2813
2870
  ${bandCase} AS band,
2814
- count() AS sessions,
2871
+ count(*) AS sessions,
2815
2872
  sum(CASE WHEN coalesce(eng.interactions, 0) = 0 THEN 1 ELSE 0 END) AS bounced
2816
2873
  FROM load_ms AS lm LEFT JOIN engaged AS eng ON lm.session_id = eng.session_id
2817
2874
  WHERE lm.load_ms IS NOT NULL
@@ -2894,7 +2951,7 @@ export function buildVariantLeaderboard(projectId, opts, d) {
2894
2951
  }
2895
2952
  // Per-view aggregates: total views and distinct sessions per variant.
2896
2953
  ctes.push(`view_counts AS (
2897
- SELECT variant, count() AS views, count(DISTINCT session_id) AS sessions
2954
+ SELECT variant, count(*) AS views, count(DISTINCT session_id) AS sessions
2898
2955
  FROM variant_views
2899
2956
  GROUP BY variant
2900
2957
  )`);