@gscdump/cloudflare 0.26.0 → 0.26.2

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.
@@ -35,6 +35,44 @@ function deviceSource(suffix) {
35
35
  sumPosition: `sum_position_${suffix}`
36
36
  };
37
37
  }
38
+ const STD_METRICS = [
39
+ "clicks",
40
+ "impressions",
41
+ "ctr",
42
+ "position"
43
+ ];
44
+ function coalesceMetric(metric, src, alias) {
45
+ const ref = `${src}.${metric}`;
46
+ return metric === "clicks" || metric === "impressions" ? `CAST(COALESCE(${ref}, 0) AS DOUBLE) AS ${alias}` : `COALESCE(${ref}, 0) AS ${alias}`;
47
+ }
48
+ function prevAlias(metric) {
49
+ return `prev${metric.charAt(0).toUpperCase()}${metric.slice(1)}`;
50
+ }
51
+ function moverClause(movers) {
52
+ const curClicks = "COALESCE(c.clicks, 0)";
53
+ const prevClicks = "COALESCE(p.clicks, 0)";
54
+ const curImpr = "COALESCE(c.impressions, 0)";
55
+ const prevImpr = "COALESCE(p.impressions, 0)";
56
+ switch (movers) {
57
+ case "improving": return {
58
+ where: `${curClicks} > ${prevClicks}`,
59
+ order: `(${curClicks} - ${prevClicks}) DESC`
60
+ };
61
+ case "declining": return {
62
+ where: `${curClicks} < ${prevClicks}`,
63
+ order: `(${curClicks} - ${prevClicks}) ASC`
64
+ };
65
+ case "new": return {
66
+ where: `${prevImpr} = 0 AND ${curImpr} > 0`,
67
+ order: `${curClicks} DESC, ${curImpr} DESC`
68
+ };
69
+ case "lost": return {
70
+ where: `${curImpr} = 0 AND ${prevImpr} > 0`,
71
+ order: `${prevImpr} DESC`
72
+ };
73
+ default: throw new Error(`[archetype-sql] unknown movers mode: ${movers}`);
74
+ }
75
+ }
38
76
  function sqlStringLiteral(value) {
39
77
  return `'${value.replace(/'/g, "''")}'`;
40
78
  }
@@ -115,15 +153,36 @@ function buildEntityDailySparkline(q) {
115
153
  function buildTopNBreakdown(q) {
116
154
  const table = inferTable([q.dimension]);
117
155
  const w = partitionWhere(q);
156
+ const order = `${q.orderBy.metric} ${q.orderBy.dir.toUpperCase()}`;
157
+ const limit = `LIMIT ${Math.max(0, Math.floor(q.limit))}`;
158
+ const offset = q.offset && q.offset > 0 ? ` OFFSET ${Math.floor(q.offset)}` : "";
159
+ const metricList = q.metrics.includes(q.orderBy.metric) ? q.metrics : [...q.metrics, q.orderBy.metric];
160
+ const variantSel = q.dimension === "queryCanonical" ? ", COUNT(DISTINCT query) AS variantCount" : "";
118
161
  if (q.dimension === "device") {
119
- const metricList = q.metrics.includes(q.orderBy.metric) ? q.metrics : [...q.metrics, q.orderBy.metric];
120
- const order = `${q.orderBy.metric} ${q.orderBy.dir.toUpperCase()}`;
121
- let sql = `${DEVICE_SUFFIXES.map((suffix) => {
162
+ if (q.compareRange) {
163
+ const wPrev = partitionWhere({
164
+ ...q,
165
+ range: q.compareRange
166
+ });
167
+ const deviceSelects = (clause, ml) => DEVICE_SUFFIXES.map((suffix) => {
168
+ const source = deviceSource(suffix);
169
+ const metrics = ml.map((m) => metricExprForSource(m, source)).join(", ");
170
+ return `SELECT '${suffix.toUpperCase()}' AS device, ${metrics} FROM ${TABLE_PLACEHOLDER} WHERE ${clause}`;
171
+ }).join(" UNION ALL ");
172
+ const curCols = metricList.map((m) => coalesceMetric(m, "c", m)).join(", ");
173
+ const prevCols = STD_METRICS.map((m) => coalesceMetric(m, "p", prevAlias(m))).join(", ");
174
+ const sql = `WITH cur AS (${deviceSelects(w.clause, metricList)}), prev AS (${deviceSelects(wPrev.clause, STD_METRICS)}) SELECT COALESCE(c.device, p.device) AS device, ${curCols}, ${prevCols} FROM cur c FULL OUTER JOIN prev p ON c.device = p.device ORDER BY ${order} ${limit}${offset}`;
175
+ return {
176
+ table,
177
+ params: [...DEVICE_SUFFIXES.flatMap(() => w.params), ...DEVICE_SUFFIXES.flatMap(() => wPrev.params)],
178
+ sql
179
+ };
180
+ }
181
+ const sql = `${DEVICE_SUFFIXES.map((suffix) => {
122
182
  const source = deviceSource(suffix);
123
183
  const metrics = metricList.map((m) => metricExprForSource(m, source)).join(", ");
124
184
  return `SELECT '${suffix.toUpperCase()}' AS device, ${metrics} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}`;
125
- }).join(" UNION ALL ")} ORDER BY ${order} LIMIT ${Math.max(0, Math.floor(q.limit))}`;
126
- if (q.offset && q.offset > 0) sql += ` OFFSET ${Math.floor(q.offset)}`;
185
+ }).join(" UNION ALL ")} ORDER BY ${order} ${limit}${offset}`;
127
186
  return {
128
187
  table,
129
188
  params: DEVICE_SUFFIXES.flatMap(() => w.params),
@@ -131,11 +190,34 @@ function buildTopNBreakdown(q) {
131
190
  };
132
191
  }
133
192
  const col = dimColumn(q.dimension);
134
- const metrics = (q.metrics.includes(q.orderBy.metric) ? q.metrics : [...q.metrics, q.orderBy.metric]).map(metricExpr).join(", ");
135
- const order = `${q.orderBy.metric} ${q.orderBy.dir.toUpperCase()}`;
136
193
  const facet = facetPredicate(q);
137
- let sql = `SELECT ${col}, ${metrics}${q.includeTotal ? ", COUNT(*) OVER() AS __total" : ""} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}${facet.sql} GROUP BY ${col} ORDER BY ${order} LIMIT ${Math.max(0, Math.floor(q.limit))}`;
138
- if (q.offset && q.offset > 0) sql += ` OFFSET ${Math.floor(q.offset)}`;
194
+ const totalCol = q.includeTotal ? ", COUNT(*) OVER() AS __total" : "";
195
+ if (q.compareRange) {
196
+ const wPrev = partitionWhere({
197
+ ...q,
198
+ range: q.compareRange
199
+ });
200
+ const curMetrics = metricList.map(metricExpr).join(", ");
201
+ const prevMetrics = STD_METRICS.map((m) => metricExpr(m)).join(", ");
202
+ const curCols = metricList.map((m) => coalesceMetric(m, "c", m)).join(", ");
203
+ const prevCols = STD_METRICS.map((m) => coalesceMetric(m, "p", prevAlias(m))).join(", ");
204
+ const variantOut = q.dimension === "queryCanonical" ? ", c.variantCount AS variantCount" : "";
205
+ const mover = q.movers ? moverClause(q.movers) : null;
206
+ const moverWhere = mover ? `WHERE ${mover.where} ` : "";
207
+ const orderSql = mover ? `ORDER BY ${mover.order}` : `ORDER BY ${order}`;
208
+ const sql = `WITH cur AS (SELECT ${col} AS k, ${curMetrics}${variantSel} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}${facet.sql} GROUP BY ${col}), prev AS (SELECT ${col} AS k, ${prevMetrics} FROM ${TABLE_PLACEHOLDER} WHERE ${wPrev.clause}${facet.sql} GROUP BY ${col}) SELECT COALESCE(c.k, p.k) AS ${q.dimension}, ${curCols}, ${prevCols}${variantOut}${totalCol} FROM cur c FULL OUTER JOIN prev p ON c.k = p.k ${moverWhere}${orderSql} ${limit}${offset}`;
209
+ return {
210
+ table,
211
+ params: [
212
+ ...w.params,
213
+ ...facet.params,
214
+ ...wPrev.params,
215
+ ...facet.params
216
+ ],
217
+ sql
218
+ };
219
+ }
220
+ const sql = `SELECT ${col}, ${metricList.map(metricExpr).join(", ")}${variantSel}${totalCol} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}${facet.sql} GROUP BY ${col} ORDER BY ${order} ${limit}${offset}`;
139
221
  return {
140
222
  table,
141
223
  params: [...w.params, ...facet.params],
@@ -230,6 +312,8 @@ function resolveServerTailEngineResult(query) {
230
312
  if (cls === "duckdb") return ok("duckdb");
231
313
  if (query.archetype === "top-n-breakdown" && query.offset && query.offset > 0) return ok("duckdb");
232
314
  if (query.archetype === "top-n-breakdown" && query.includeTotal) return ok("duckdb");
315
+ if (query.archetype === "top-n-breakdown" && query.compareRange) return ok("duckdb");
316
+ if (query.archetype === "top-n-breakdown" && query.dimension === "queryCanonical") return ok("duckdb");
233
317
  const facets = query.facets;
234
318
  if (facets && facets.length > 0) return ok("duckdb");
235
319
  return ok("r2-sql");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cloudflare",
3
3
  "type": "module",
4
- "version": "0.26.0",
4
+ "version": "0.26.2",
5
5
  "description": "Cloudflare-Workers-flavored helpers for the gscdump analytics stack: AnalyticsEnv binding contract, R2 SigV4 presigner, size-hint HMAC, DuckDB Workers shims, engine factory.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -46,18 +46,18 @@
46
46
  "dependencies": {
47
47
  "@uwdata/flechette": "^2.5.0",
48
48
  "aws4fetch": "^1.0.20",
49
- "@gscdump/contracts": "0.26.0",
50
- "@gscdump/engine-sqlite": "0.26.0",
51
- "@gscdump/sdk": "0.26.0",
52
- "@gscdump/engine": "0.26.0",
53
- "gscdump": "0.26.0"
49
+ "@gscdump/engine": "0.26.2",
50
+ "@gscdump/contracts": "0.26.2",
51
+ "@gscdump/engine-sqlite": "0.26.2",
52
+ "gscdump": "0.26.2",
53
+ "@gscdump/sdk": "0.26.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@cloudflare/vitest-pool-workers": "^0.16.12",
57
- "@cloudflare/workers-types": "^4.20260603.1",
56
+ "@cloudflare/vitest-pool-workers": "^0.16.13",
57
+ "@cloudflare/workers-types": "^4.20260608.1",
58
58
  "h3": "^1.15.11",
59
59
  "typescript": "^6.0.3",
60
- "wrangler": "^4.97.0"
60
+ "wrangler": "^4.98.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "obuild",