@gscdump/analysis 0.19.2 → 0.19.4

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.
@@ -2115,6 +2115,142 @@ function buildDataQueryPlan(params, options) {
2115
2115
  extraQueries
2116
2116
  };
2117
2117
  }
2118
+ function totalsState(state) {
2119
+ return {
2120
+ ...state,
2121
+ dimensions: [],
2122
+ orderBy: void 0,
2123
+ rowLimit: 1
2124
+ };
2125
+ }
2126
+ function readTotals(row) {
2127
+ return {
2128
+ clicks: Number(row?.clicks ?? 0),
2129
+ impressions: Number(row?.impressions ?? 0),
2130
+ ctr: Number(row?.ctr ?? 0),
2131
+ position: Number(row?.position ?? 0)
2132
+ };
2133
+ }
2134
+ function buildDataQueryRows(params) {
2135
+ const state = requireBuilderState(params.q, "data-query");
2136
+ if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
2137
+ const queries = {
2138
+ main: state,
2139
+ totals: totalsState(state)
2140
+ };
2141
+ const prev = optionalBuilderState(params.qc, "data-query", "qc");
2142
+ if (prev) {
2143
+ queries.prevMain = prev;
2144
+ queries.prevTotals = totalsState(prev);
2145
+ }
2146
+ return queries;
2147
+ }
2148
+ function shapeDataQueryRowResults(rowMap, params) {
2149
+ const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
2150
+ const totals = readTotals(rowMap.totals?.[0]);
2151
+ if (params.qc == null) return {
2152
+ results: main,
2153
+ meta: {
2154
+ totalCount: main.length,
2155
+ totals
2156
+ }
2157
+ };
2158
+ const state = requireBuilderState(params.q, "data-query");
2159
+ const dims = state.dimensions;
2160
+ const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
2161
+ const prevByKey = /* @__PURE__ */ new Map();
2162
+ for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
2163
+ const filter = params.comparisonFilter;
2164
+ const merged = [];
2165
+ const seen = /* @__PURE__ */ new Set();
2166
+ for (const cur of main) {
2167
+ const key = keyOf(cur);
2168
+ seen.add(key);
2169
+ const prev = prevByKey.get(key);
2170
+ const row = {
2171
+ ...cur,
2172
+ prevClicks: Number(prev?.clicks ?? 0),
2173
+ prevImpressions: Number(prev?.impressions ?? 0),
2174
+ prevCtr: Number(prev?.ctr ?? 0),
2175
+ prevPosition: Number(prev?.position ?? 0)
2176
+ };
2177
+ if (passesComparisonFilter(filter, {
2178
+ isNew: !prev,
2179
+ isLost: false,
2180
+ clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
2181
+ })) merged.push(row);
2182
+ }
2183
+ for (const prev of rowMap.prevMain ?? []) {
2184
+ const p = prev;
2185
+ const key = keyOf(p);
2186
+ if (seen.has(key)) continue;
2187
+ const row = {
2188
+ ...p,
2189
+ clicks: 0,
2190
+ impressions: 0,
2191
+ ctr: 0,
2192
+ position: 0,
2193
+ prevClicks: Number(p.clicks ?? 0),
2194
+ prevImpressions: Number(p.impressions ?? 0),
2195
+ prevCtr: Number(p.ctr ?? 0),
2196
+ prevPosition: Number(p.position ?? 0)
2197
+ };
2198
+ if (passesComparisonFilter(filter, {
2199
+ isNew: false,
2200
+ isLost: true,
2201
+ clicksChange: -Number(p.clicks ?? 0)
2202
+ })) merged.push(row);
2203
+ }
2204
+ if (state.orderBy) {
2205
+ const { column, dir } = state.orderBy;
2206
+ merged.sort((a, b) => {
2207
+ const av = Number(a[column]) || 0;
2208
+ const bv = Number(b[column]) || 0;
2209
+ return dir === "asc" ? av - bv : bv - av;
2210
+ });
2211
+ }
2212
+ return {
2213
+ results: merged.map((r) => coerceNumericCols(r)),
2214
+ meta: {
2215
+ totalCount: merged.length,
2216
+ totals
2217
+ }
2218
+ };
2219
+ }
2220
+ function passesComparisonFilter(filter, ctx) {
2221
+ switch (filter) {
2222
+ case "new": return ctx.isNew;
2223
+ case "lost": return ctx.isLost;
2224
+ case "improving": return ctx.clicksChange > 0;
2225
+ case "declining": return ctx.clicksChange < 0;
2226
+ default: return true;
2227
+ }
2228
+ }
2229
+ function buildDataDetailRows(params) {
2230
+ const state = requireBuilderState(params.q, "data-detail");
2231
+ if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
2232
+ const queries = {
2233
+ main: state,
2234
+ totals: totalsState(state)
2235
+ };
2236
+ const prev = optionalBuilderState(params.qc, "data-detail", "qc");
2237
+ if (prev) queries.prevTotals = totalsState(prev);
2238
+ return queries;
2239
+ }
2240
+ function shapeDataDetailRowResults(rowMap, params) {
2241
+ const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
2242
+ const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
2243
+ const daily = startDate && endDate ? padTimeseries(coerced, {
2244
+ startDate,
2245
+ endDate
2246
+ }) : coerced;
2247
+ const meta = { totals: readTotals(rowMap.totals?.[0]) };
2248
+ if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
2249
+ return {
2250
+ results: daily,
2251
+ meta
2252
+ };
2253
+ }
2118
2254
  function shapeDataQueryRows(rows, params, extras) {
2119
2255
  return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
2120
2256
  }
@@ -2200,6 +2336,16 @@ const dataDetailAnalyzer = defineAnalyzer({
2200
2336
  results,
2201
2337
  meta
2202
2338
  };
2339
+ },
2340
+ buildRows(params) {
2341
+ return buildDataDetailRows(params);
2342
+ },
2343
+ reduceRows(rows, params) {
2344
+ const { results, meta } = shapeDataDetailRowResults(Array.isArray(rows) ? {} : rows, params);
2345
+ return {
2346
+ results,
2347
+ meta
2348
+ };
2203
2349
  }
2204
2350
  });
2205
2351
  const dataQueryAnalyzer = defineAnalyzer({
@@ -2230,6 +2376,16 @@ const dataQueryAnalyzer = defineAnalyzer({
2230
2376
  results,
2231
2377
  meta
2232
2378
  };
2379
+ },
2380
+ buildRows(params) {
2381
+ return buildDataQueryRows(params);
2382
+ },
2383
+ reduceRows(rows, params) {
2384
+ const { results, meta } = shapeDataQueryRowResults(Array.isArray(rows) ? {} : rows, params);
2385
+ return {
2386
+ results,
2387
+ meta
2388
+ };
2233
2389
  }
2234
2390
  });
2235
2391
  const sortResults$1 = createMetricSorter("lostClicks", {
@@ -2115,6 +2115,142 @@ function buildDataQueryPlan(params, options) {
2115
2115
  extraQueries
2116
2116
  };
2117
2117
  }
2118
+ function totalsState(state) {
2119
+ return {
2120
+ ...state,
2121
+ dimensions: [],
2122
+ orderBy: void 0,
2123
+ rowLimit: 1
2124
+ };
2125
+ }
2126
+ function readTotals(row) {
2127
+ return {
2128
+ clicks: Number(row?.clicks ?? 0),
2129
+ impressions: Number(row?.impressions ?? 0),
2130
+ ctr: Number(row?.ctr ?? 0),
2131
+ position: Number(row?.position ?? 0)
2132
+ };
2133
+ }
2134
+ function buildDataQueryRows(params) {
2135
+ const state = requireBuilderState(params.q, "data-query");
2136
+ if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
2137
+ const queries = {
2138
+ main: state,
2139
+ totals: totalsState(state)
2140
+ };
2141
+ const prev = optionalBuilderState(params.qc, "data-query", "qc");
2142
+ if (prev) {
2143
+ queries.prevMain = prev;
2144
+ queries.prevTotals = totalsState(prev);
2145
+ }
2146
+ return queries;
2147
+ }
2148
+ function shapeDataQueryRowResults(rowMap, params) {
2149
+ const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
2150
+ const totals = readTotals(rowMap.totals?.[0]);
2151
+ if (params.qc == null) return {
2152
+ results: main,
2153
+ meta: {
2154
+ totalCount: main.length,
2155
+ totals
2156
+ }
2157
+ };
2158
+ const state = requireBuilderState(params.q, "data-query");
2159
+ const dims = state.dimensions;
2160
+ const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
2161
+ const prevByKey = /* @__PURE__ */ new Map();
2162
+ for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
2163
+ const filter = params.comparisonFilter;
2164
+ const merged = [];
2165
+ const seen = /* @__PURE__ */ new Set();
2166
+ for (const cur of main) {
2167
+ const key = keyOf(cur);
2168
+ seen.add(key);
2169
+ const prev = prevByKey.get(key);
2170
+ const row = {
2171
+ ...cur,
2172
+ prevClicks: Number(prev?.clicks ?? 0),
2173
+ prevImpressions: Number(prev?.impressions ?? 0),
2174
+ prevCtr: Number(prev?.ctr ?? 0),
2175
+ prevPosition: Number(prev?.position ?? 0)
2176
+ };
2177
+ if (passesComparisonFilter(filter, {
2178
+ isNew: !prev,
2179
+ isLost: false,
2180
+ clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
2181
+ })) merged.push(row);
2182
+ }
2183
+ for (const prev of rowMap.prevMain ?? []) {
2184
+ const p = prev;
2185
+ const key = keyOf(p);
2186
+ if (seen.has(key)) continue;
2187
+ const row = {
2188
+ ...p,
2189
+ clicks: 0,
2190
+ impressions: 0,
2191
+ ctr: 0,
2192
+ position: 0,
2193
+ prevClicks: Number(p.clicks ?? 0),
2194
+ prevImpressions: Number(p.impressions ?? 0),
2195
+ prevCtr: Number(p.ctr ?? 0),
2196
+ prevPosition: Number(p.position ?? 0)
2197
+ };
2198
+ if (passesComparisonFilter(filter, {
2199
+ isNew: false,
2200
+ isLost: true,
2201
+ clicksChange: -Number(p.clicks ?? 0)
2202
+ })) merged.push(row);
2203
+ }
2204
+ if (state.orderBy) {
2205
+ const { column, dir } = state.orderBy;
2206
+ merged.sort((a, b) => {
2207
+ const av = Number(a[column]) || 0;
2208
+ const bv = Number(b[column]) || 0;
2209
+ return dir === "asc" ? av - bv : bv - av;
2210
+ });
2211
+ }
2212
+ return {
2213
+ results: merged.map((r) => coerceNumericCols(r)),
2214
+ meta: {
2215
+ totalCount: merged.length,
2216
+ totals
2217
+ }
2218
+ };
2219
+ }
2220
+ function passesComparisonFilter(filter, ctx) {
2221
+ switch (filter) {
2222
+ case "new": return ctx.isNew;
2223
+ case "lost": return ctx.isLost;
2224
+ case "improving": return ctx.clicksChange > 0;
2225
+ case "declining": return ctx.clicksChange < 0;
2226
+ default: return true;
2227
+ }
2228
+ }
2229
+ function buildDataDetailRows(params) {
2230
+ const state = requireBuilderState(params.q, "data-detail");
2231
+ if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
2232
+ const queries = {
2233
+ main: state,
2234
+ totals: totalsState(state)
2235
+ };
2236
+ const prev = optionalBuilderState(params.qc, "data-detail", "qc");
2237
+ if (prev) queries.prevTotals = totalsState(prev);
2238
+ return queries;
2239
+ }
2240
+ function shapeDataDetailRowResults(rowMap, params) {
2241
+ const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
2242
+ const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
2243
+ const daily = startDate && endDate ? padTimeseries(coerced, {
2244
+ startDate,
2245
+ endDate
2246
+ }) : coerced;
2247
+ const meta = { totals: readTotals(rowMap.totals?.[0]) };
2248
+ if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
2249
+ return {
2250
+ results: daily,
2251
+ meta
2252
+ };
2253
+ }
2118
2254
  function shapeDataQueryRows(rows, params, extras) {
2119
2255
  return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
2120
2256
  }
@@ -2200,6 +2336,16 @@ const dataDetailAnalyzer = defineAnalyzer({
2200
2336
  results,
2201
2337
  meta
2202
2338
  };
2339
+ },
2340
+ buildRows(params) {
2341
+ return buildDataDetailRows(params);
2342
+ },
2343
+ reduceRows(rows, params) {
2344
+ const { results, meta } = shapeDataDetailRowResults(Array.isArray(rows) ? {} : rows, params);
2345
+ return {
2346
+ results,
2347
+ meta
2348
+ };
2203
2349
  }
2204
2350
  });
2205
2351
  const dataQueryAnalyzer = defineAnalyzer({
@@ -2230,6 +2376,16 @@ const dataQueryAnalyzer = defineAnalyzer({
2230
2376
  results,
2231
2377
  meta
2232
2378
  };
2379
+ },
2380
+ buildRows(params) {
2381
+ return buildDataQueryRows(params);
2382
+ },
2383
+ reduceRows(rows, params) {
2384
+ const { results, meta } = shapeDataQueryRowResults(Array.isArray(rows) ? {} : rows, params);
2385
+ return {
2386
+ results,
2387
+ meta
2388
+ };
2233
2389
  }
2234
2390
  });
2235
2391
  const sortResults$1 = createMetricSorter("lostClicks", {
package/dist/index.mjs CHANGED
@@ -9,6 +9,7 @@ import { buildExtrasQueries, buildTotalsSql, mergeExtras, pgResolverAdapter, res
9
9
  import { ENGINE_QUERY_CAPABILITIES, createAttachedTableSource, createEngineQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery } from "@gscdump/engine/source";
10
10
  import { computeInputHash, createReportRegistry, defineReport } from "@gscdump/engine/report";
11
11
  import { canProxyToGsc } from "@gscdump/engine-gsc-api";
12
+ import { isStateResolvable } from "gscdump/query/plan";
12
13
  function clamp01(value) {
13
14
  if (value < 0) return 0;
14
15
  if (value > 1) return 1;
@@ -2331,6 +2332,142 @@ function buildDataQueryPlan(params, options) {
2331
2332
  extraQueries
2332
2333
  };
2333
2334
  }
2335
+ function totalsState(state) {
2336
+ return {
2337
+ ...state,
2338
+ dimensions: [],
2339
+ orderBy: void 0,
2340
+ rowLimit: 1
2341
+ };
2342
+ }
2343
+ function readTotals(row) {
2344
+ return {
2345
+ clicks: Number(row?.clicks ?? 0),
2346
+ impressions: Number(row?.impressions ?? 0),
2347
+ ctr: Number(row?.ctr ?? 0),
2348
+ position: Number(row?.position ?? 0)
2349
+ };
2350
+ }
2351
+ function buildDataQueryRows(params) {
2352
+ const state = requireBuilderState(params.q, "data-query");
2353
+ if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
2354
+ const queries = {
2355
+ main: state,
2356
+ totals: totalsState(state)
2357
+ };
2358
+ const prev = optionalBuilderState(params.qc, "data-query", "qc");
2359
+ if (prev) {
2360
+ queries.prevMain = prev;
2361
+ queries.prevTotals = totalsState(prev);
2362
+ }
2363
+ return queries;
2364
+ }
2365
+ function shapeDataQueryRowResults(rowMap, params) {
2366
+ const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
2367
+ const totals = readTotals(rowMap.totals?.[0]);
2368
+ if (params.qc == null) return {
2369
+ results: main,
2370
+ meta: {
2371
+ totalCount: main.length,
2372
+ totals
2373
+ }
2374
+ };
2375
+ const state = requireBuilderState(params.q, "data-query");
2376
+ const dims = state.dimensions;
2377
+ const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
2378
+ const prevByKey = /* @__PURE__ */ new Map();
2379
+ for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
2380
+ const filter = params.comparisonFilter;
2381
+ const merged = [];
2382
+ const seen = /* @__PURE__ */ new Set();
2383
+ for (const cur of main) {
2384
+ const key = keyOf(cur);
2385
+ seen.add(key);
2386
+ const prev = prevByKey.get(key);
2387
+ const row = {
2388
+ ...cur,
2389
+ prevClicks: Number(prev?.clicks ?? 0),
2390
+ prevImpressions: Number(prev?.impressions ?? 0),
2391
+ prevCtr: Number(prev?.ctr ?? 0),
2392
+ prevPosition: Number(prev?.position ?? 0)
2393
+ };
2394
+ if (passesComparisonFilter(filter, {
2395
+ isNew: !prev,
2396
+ isLost: false,
2397
+ clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
2398
+ })) merged.push(row);
2399
+ }
2400
+ for (const prev of rowMap.prevMain ?? []) {
2401
+ const p = prev;
2402
+ const key = keyOf(p);
2403
+ if (seen.has(key)) continue;
2404
+ const row = {
2405
+ ...p,
2406
+ clicks: 0,
2407
+ impressions: 0,
2408
+ ctr: 0,
2409
+ position: 0,
2410
+ prevClicks: Number(p.clicks ?? 0),
2411
+ prevImpressions: Number(p.impressions ?? 0),
2412
+ prevCtr: Number(p.ctr ?? 0),
2413
+ prevPosition: Number(p.position ?? 0)
2414
+ };
2415
+ if (passesComparisonFilter(filter, {
2416
+ isNew: false,
2417
+ isLost: true,
2418
+ clicksChange: -Number(p.clicks ?? 0)
2419
+ })) merged.push(row);
2420
+ }
2421
+ if (state.orderBy) {
2422
+ const { column, dir } = state.orderBy;
2423
+ merged.sort((a, b) => {
2424
+ const av = Number(a[column]) || 0;
2425
+ const bv = Number(b[column]) || 0;
2426
+ return dir === "asc" ? av - bv : bv - av;
2427
+ });
2428
+ }
2429
+ return {
2430
+ results: merged.map((r) => coerceNumericCols(r)),
2431
+ meta: {
2432
+ totalCount: merged.length,
2433
+ totals
2434
+ }
2435
+ };
2436
+ }
2437
+ function passesComparisonFilter(filter, ctx) {
2438
+ switch (filter) {
2439
+ case "new": return ctx.isNew;
2440
+ case "lost": return ctx.isLost;
2441
+ case "improving": return ctx.clicksChange > 0;
2442
+ case "declining": return ctx.clicksChange < 0;
2443
+ default: return true;
2444
+ }
2445
+ }
2446
+ function buildDataDetailRows(params) {
2447
+ const state = requireBuilderState(params.q, "data-detail");
2448
+ if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
2449
+ const queries = {
2450
+ main: state,
2451
+ totals: totalsState(state)
2452
+ };
2453
+ const prev = optionalBuilderState(params.qc, "data-detail", "qc");
2454
+ if (prev) queries.prevTotals = totalsState(prev);
2455
+ return queries;
2456
+ }
2457
+ function shapeDataDetailRowResults(rowMap, params) {
2458
+ const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
2459
+ const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
2460
+ const daily = startDate && endDate ? padTimeseries$1(coerced, {
2461
+ startDate,
2462
+ endDate
2463
+ }) : coerced;
2464
+ const meta = { totals: readTotals(rowMap.totals?.[0]) };
2465
+ if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
2466
+ return {
2467
+ results: daily,
2468
+ meta
2469
+ };
2470
+ }
2334
2471
  function shapeDataQueryRows(rows, params, extras) {
2335
2472
  return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
2336
2473
  }
@@ -2565,6 +2702,16 @@ const dataDetailAnalyzer = defineAnalyzer$1({
2565
2702
  results,
2566
2703
  meta
2567
2704
  };
2705
+ },
2706
+ buildRows(params) {
2707
+ return buildDataDetailRows(params);
2708
+ },
2709
+ reduceRows(rows, params) {
2710
+ const { results, meta } = shapeDataDetailRowResults(Array.isArray(rows) ? {} : rows, params);
2711
+ return {
2712
+ results,
2713
+ meta
2714
+ };
2568
2715
  }
2569
2716
  });
2570
2717
  const dataQueryAnalyzer = defineAnalyzer$1({
@@ -2595,6 +2742,16 @@ const dataQueryAnalyzer = defineAnalyzer$1({
2595
2742
  results,
2596
2743
  meta
2597
2744
  };
2745
+ },
2746
+ buildRows(params) {
2747
+ return buildDataQueryRows(params);
2748
+ },
2749
+ reduceRows(rows, params) {
2750
+ const { results, meta } = shapeDataQueryRowResults(Array.isArray(rows) ? {} : rows, params);
2751
+ return {
2752
+ results,
2753
+ meta
2754
+ };
2598
2755
  }
2599
2756
  });
2600
2757
  const sortResults$1 = createMetricSorter("lostClicks", {
@@ -6767,6 +6924,7 @@ function nextDay(day) {
6767
6924
  }
6768
6925
  function shouldRouteToLive(state, site) {
6769
6926
  if (!canProxyToGsc(state)) return false;
6927
+ if (!isStateResolvable(state)) return true;
6770
6928
  const { startDate, endDate } = extractDateRange(state.filter);
6771
6929
  if (!startDate || !endDate) return false;
6772
6930
  if (!site.oldestDateSynced || !site.newestDateSynced) return true;
@@ -1,4 +1,5 @@
1
1
  import { ResolverOptions } from "@gscdump/engine/resolver";
2
+ import { BuilderState } from "gscdump/query";
2
3
  import { AnalysisParams } from "@gscdump/engine/analysis-types";
3
4
  import { Row } from "@gscdump/engine/contracts";
4
5
  interface QueryAnalyzerExtraQuery {
@@ -13,6 +14,20 @@ interface QueryAnalyzerPlan<TK extends string = string> {
13
14
  extraQueries?: QueryAnalyzerExtraQuery[];
14
15
  }
15
16
  declare function buildDataQueryPlan<TK extends string>(params: AnalysisParams, options: ResolverOptions<TK>): QueryAnalyzerPlan<TK>;
17
+ /** Row-query plan for `data-query`. Comparison (`qc`) joins two periods. */
18
+ declare function buildDataQueryRows(params: AnalysisParams): Record<string, BuilderState>;
19
+ /** Post-processing for `data-query` row results. Mirrors `shapeDataQuery`. */
20
+ declare function shapeDataQueryRowResults(rowMap: Record<string, Row[]>, params: AnalysisParams): {
21
+ results: Row[];
22
+ meta: Record<string, unknown>;
23
+ };
24
+ /** Row-query plan for `data-detail`. `qc` adds a previous-period totals query. */
25
+ declare function buildDataDetailRows(params: AnalysisParams): Record<string, BuilderState>;
26
+ /** Post-processing for `data-detail` row results. Mirrors `shapeDataDetailRows`. */
27
+ declare function shapeDataDetailRowResults(rowMap: Record<string, Row[]>, params: AnalysisParams): {
28
+ results: Row[];
29
+ meta: Record<string, unknown>;
30
+ };
16
31
  /**
17
32
  * Pure post-processing for `data-query` rows. Adapter-free so reducers can
18
33
  * call it without re-running the SQL plan.
@@ -35,4 +50,4 @@ declare function shapeDataDetailRows(rows: Row[], params: AnalysisParams, extras
35
50
  * Idempotent: `normalizeQuery(normalizeQuery(q)) === normalizeQuery(q)`.
36
51
  */
37
52
  declare function normalizeQuery(query: string): string;
38
- export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataQueryPlan, normalizeQuery, shapeDataDetailRows, shapeDataQueryRows };
53
+ export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataDetailRows, buildDataQueryPlan, buildDataQueryRows, normalizeQuery, shapeDataDetailRowResults, shapeDataDetailRows, shapeDataQueryRowResults, shapeDataQueryRows };
@@ -99,6 +99,142 @@ function buildDataQueryPlan(params, options) {
99
99
  extraQueries
100
100
  };
101
101
  }
102
+ function totalsState(state) {
103
+ return {
104
+ ...state,
105
+ dimensions: [],
106
+ orderBy: void 0,
107
+ rowLimit: 1
108
+ };
109
+ }
110
+ function readTotals(row) {
111
+ return {
112
+ clicks: Number(row?.clicks ?? 0),
113
+ impressions: Number(row?.impressions ?? 0),
114
+ ctr: Number(row?.ctr ?? 0),
115
+ position: Number(row?.position ?? 0)
116
+ };
117
+ }
118
+ function buildDataQueryRows(params) {
119
+ const state = requireBuilderState(params.q, "data-query");
120
+ if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
121
+ const queries = {
122
+ main: state,
123
+ totals: totalsState(state)
124
+ };
125
+ const prev = optionalBuilderState(params.qc, "data-query", "qc");
126
+ if (prev) {
127
+ queries.prevMain = prev;
128
+ queries.prevTotals = totalsState(prev);
129
+ }
130
+ return queries;
131
+ }
132
+ function shapeDataQueryRowResults(rowMap, params) {
133
+ const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
134
+ const totals = readTotals(rowMap.totals?.[0]);
135
+ if (params.qc == null) return {
136
+ results: main,
137
+ meta: {
138
+ totalCount: main.length,
139
+ totals
140
+ }
141
+ };
142
+ const state = requireBuilderState(params.q, "data-query");
143
+ const dims = state.dimensions;
144
+ const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
145
+ const prevByKey = /* @__PURE__ */ new Map();
146
+ for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
147
+ const filter = params.comparisonFilter;
148
+ const merged = [];
149
+ const seen = /* @__PURE__ */ new Set();
150
+ for (const cur of main) {
151
+ const key = keyOf(cur);
152
+ seen.add(key);
153
+ const prev = prevByKey.get(key);
154
+ const row = {
155
+ ...cur,
156
+ prevClicks: Number(prev?.clicks ?? 0),
157
+ prevImpressions: Number(prev?.impressions ?? 0),
158
+ prevCtr: Number(prev?.ctr ?? 0),
159
+ prevPosition: Number(prev?.position ?? 0)
160
+ };
161
+ if (passesComparisonFilter(filter, {
162
+ isNew: !prev,
163
+ isLost: false,
164
+ clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
165
+ })) merged.push(row);
166
+ }
167
+ for (const prev of rowMap.prevMain ?? []) {
168
+ const p = prev;
169
+ const key = keyOf(p);
170
+ if (seen.has(key)) continue;
171
+ const row = {
172
+ ...p,
173
+ clicks: 0,
174
+ impressions: 0,
175
+ ctr: 0,
176
+ position: 0,
177
+ prevClicks: Number(p.clicks ?? 0),
178
+ prevImpressions: Number(p.impressions ?? 0),
179
+ prevCtr: Number(p.ctr ?? 0),
180
+ prevPosition: Number(p.position ?? 0)
181
+ };
182
+ if (passesComparisonFilter(filter, {
183
+ isNew: false,
184
+ isLost: true,
185
+ clicksChange: -Number(p.clicks ?? 0)
186
+ })) merged.push(row);
187
+ }
188
+ if (state.orderBy) {
189
+ const { column, dir } = state.orderBy;
190
+ merged.sort((a, b) => {
191
+ const av = Number(a[column]) || 0;
192
+ const bv = Number(b[column]) || 0;
193
+ return dir === "asc" ? av - bv : bv - av;
194
+ });
195
+ }
196
+ return {
197
+ results: merged.map((r) => coerceNumericCols(r)),
198
+ meta: {
199
+ totalCount: merged.length,
200
+ totals
201
+ }
202
+ };
203
+ }
204
+ function passesComparisonFilter(filter, ctx) {
205
+ switch (filter) {
206
+ case "new": return ctx.isNew;
207
+ case "lost": return ctx.isLost;
208
+ case "improving": return ctx.clicksChange > 0;
209
+ case "declining": return ctx.clicksChange < 0;
210
+ default: return true;
211
+ }
212
+ }
213
+ function buildDataDetailRows(params) {
214
+ const state = requireBuilderState(params.q, "data-detail");
215
+ if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
216
+ const queries = {
217
+ main: state,
218
+ totals: totalsState(state)
219
+ };
220
+ const prev = optionalBuilderState(params.qc, "data-detail", "qc");
221
+ if (prev) queries.prevTotals = totalsState(prev);
222
+ return queries;
223
+ }
224
+ function shapeDataDetailRowResults(rowMap, params) {
225
+ const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
226
+ const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
227
+ const daily = startDate && endDate ? padTimeseries(coerced, {
228
+ startDate,
229
+ endDate
230
+ }) : coerced;
231
+ const meta = { totals: readTotals(rowMap.totals?.[0]) };
232
+ if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
233
+ return {
234
+ results: daily,
235
+ meta
236
+ };
237
+ }
102
238
  function shapeDataQueryRows(rows, params, extras) {
103
239
  return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
104
240
  }
@@ -305,4 +441,4 @@ const WHITESPACE_RE = /\s+/g;
305
441
  function normalizeQuery(query) {
306
442
  return query.toLowerCase().replace(SEPARATOR_RE, " ").replace(WHITESPACE_RE, " ").trim().split(" ").filter(Boolean).map((token) => SYNONYMS[token] ?? token).filter(Boolean).map(depluralize).sort().join(" ");
307
443
  }
308
- export { buildDataDetailPlan, buildDataQueryPlan, normalizeQuery, shapeDataDetailRows, shapeDataQueryRows };
444
+ export { buildDataDetailPlan, buildDataDetailRows, buildDataQueryPlan, buildDataQueryRows, normalizeQuery, shapeDataDetailRowResults, shapeDataDetailRows, shapeDataQueryRowResults, shapeDataQueryRows };
@@ -1,6 +1,6 @@
1
1
  import { BuilderState } from "gscdump/query";
2
- import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
3
2
  import { PlannerCapabilities } from "gscdump/query/plan";
3
+ import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
4
4
  interface SyncedRange {
5
5
  oldestDateSynced: string | null;
6
6
  newestDateSynced: string | null;
@@ -1,5 +1,6 @@
1
1
  import { canProxyToGsc } from "@gscdump/engine-gsc-api";
2
2
  import { extractDateRange } from "gscdump/query";
3
+ import { isStateResolvable } from "gscdump/query/plan";
3
4
  function hasGapInCoveredSpans(start, end, coveredSpans) {
4
5
  let cursor = start;
5
6
  for (const span of coveredSpans) {
@@ -17,6 +18,7 @@ function nextDay(day) {
17
18
  }
18
19
  function shouldRouteToLive(state, site) {
19
20
  if (!canProxyToGsc(state)) return false;
21
+ if (!isStateResolvable(state)) return true;
20
22
  const { startDate, endDate } = extractDateRange(state.filter);
21
23
  if (!startDate || !endDate) return false;
22
24
  if (!site.oldestDateSynced || !site.newestDateSynced) return true;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "0.19.2",
4
+ "version": "0.19.4",
5
5
  "description": "GSC analyzers — striking-distance, opportunity, movers, decay, brand, clustering, concentration, seasonality. Pure row-based + DuckDB-native.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -75,9 +75,9 @@
75
75
  },
76
76
  "dependencies": {
77
77
  "drizzle-orm": "^0.45.2",
78
- "@gscdump/engine": "0.19.2",
79
- "@gscdump/engine-gsc-api": "0.19.2",
80
- "gscdump": "0.19.2"
78
+ "@gscdump/engine": "0.19.4",
79
+ "@gscdump/engine-gsc-api": "0.19.4",
80
+ "gscdump": "0.19.4"
81
81
  },
82
82
  "devDependencies": {
83
83
  "vitest": "^4.1.6"