@gscdump/analysis 0.19.3 → 0.19.6

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
@@ -2332,6 +2332,142 @@ function buildDataQueryPlan(params, options) {
2332
2332
  extraQueries
2333
2333
  };
2334
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
+ }
2335
2471
  function shapeDataQueryRows(rows, params, extras) {
2336
2472
  return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
2337
2473
  }
@@ -2566,6 +2702,16 @@ const dataDetailAnalyzer = defineAnalyzer$1({
2566
2702
  results,
2567
2703
  meta
2568
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
+ };
2569
2715
  }
2570
2716
  });
2571
2717
  const dataQueryAnalyzer = defineAnalyzer$1({
@@ -2596,6 +2742,16 @@ const dataQueryAnalyzer = defineAnalyzer$1({
2596
2742
  results,
2597
2743
  meta
2598
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
+ };
2599
2755
  }
2600
2756
  });
2601
2757
  const sortResults$1 = createMetricSorter("lostClicks", {
@@ -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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "0.19.3",
4
+ "version": "0.19.6",
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.3",
79
- "@gscdump/engine-gsc-api": "0.19.3",
80
- "gscdump": "0.19.3"
78
+ "@gscdump/engine": "0.19.6",
79
+ "@gscdump/engine-gsc-api": "0.19.6",
80
+ "gscdump": "0.19.6"
81
81
  },
82
82
  "devDependencies": {
83
83
  "vitest": "^4.1.6"