@gscdump/engine 5.6.0 → 5.7.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.
@@ -1,3 +1,4 @@
1
+ import { ComparisonOrderBy } from "./resolver/types.mjs";
1
2
  import { BuilderState, SearchType } from "gscdump/query";
2
3
  export type AnalysisTool = 'striking-distance' | 'opportunity' | 'movers' | 'decay' | 'zero-click' | 'brand' | 'cannibalization' | 'clustering' | 'concentration' | 'seasonality' | 'trends' | 'ctr-anomaly' | 'position-volatility' | 'long-tail' | 'intent-atlas' | 'query-migration' | 'bayesian-ctr' | 'stl-decompose' | 'change-point' | 'bipartite-pagerank' | 'survival' | 'position-distribution' | 'ctr-curve' | 'dark-traffic' | 'content-velocity' | 'keyword-breadth' | 'device-gap' | 'data-query' | 'data-detail' | 'trajectory';
3
4
  export interface AnalysisParams {
@@ -55,6 +56,8 @@ export interface AnalysisParams {
55
56
  qc?: BuilderState;
56
57
  /** data-query comparison filter applied to joined current/previous rows. */
57
58
  comparisonFilter?: 'new' | 'lost' | 'improving' | 'declining';
59
+ /** data-query comparison order applied to the joined rows before the row cap. */
60
+ comparisonOrderBy?: ComparisonOrderBy;
58
61
  /** GSC slice the analysis is scoped to. Undefined = analyzer runs cross-type (today's behaviour for web-only sites). */
59
62
  searchType?: SearchType;
60
63
  }
@@ -1,6 +1,6 @@
1
1
  import { Row } from "../storage.mjs";
2
- import { AnalysisParams } from "../analysis-types.mjs";
3
2
  import { ResolverAdapter } from "../resolver/types.mjs";
3
+ import { AnalysisParams } from "../analysis-types.mjs";
4
4
  import { FileSet, SourceCapabilities } from "../source/source-types.mjs";
5
5
  import { BuilderState } from "gscdump/query";
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { ComparisonFilter, ExtraQuery, ResolvedComparisonSQL, ResolvedSQL, ResolvedSQLOptimized, ResolverOptions } from "./types.mjs";
1
+ import { ComparisonFilter, ComparisonOrderBy, ExtraQuery, ResolvedComparisonSQL, ResolvedSQL, ResolvedSQLOptimized, ResolverOptions } from "./types.mjs";
2
2
  import { BuilderState } from "gscdump/query";
3
3
  export declare function resolveToSQLOptimized<TK extends string>(state: BuilderState, options: ResolverOptions<TK>): ResolvedSQLOptimized;
4
4
  export declare function resolveToSQL<TK extends string>(state: BuilderState, options: ResolverOptions<TK>): ResolvedSQL;
@@ -6,7 +6,7 @@ export declare function buildTotalsSql<TK extends string>(state: BuilderState, o
6
6
  sql: string;
7
7
  params: unknown[];
8
8
  };
9
- export declare function resolveComparisonSQL<TK extends string>(current: BuilderState, previous: BuilderState, options: ResolverOptions<TK>, comparisonFilter?: ComparisonFilter): ResolvedComparisonSQL;
9
+ export declare function resolveComparisonSQL<TK extends string>(current: BuilderState, previous: BuilderState, options: ResolverOptions<TK>, comparisonFilter?: ComparisonFilter, comparisonOrderBy?: ComparisonOrderBy): ResolvedComparisonSQL;
10
10
  /**
11
11
  * Enrich Canonical Query groups with their raw variants.
12
12
  * Supply selected keys to limit aggregation to the current page.
@@ -35,6 +35,14 @@ function orderByClause(state, prefix = "", columnOverride) {
35
35
  }
36
36
  return sql.raw(`ORDER BY ${prefix}clicks DESC`);
37
37
  }
38
+ function changeOrderClause(order) {
39
+ if (order.column !== "clicksChange") throw new Error(`[resolver] unknown comparison order column: ${JSON.stringify(order.column)}`);
40
+ if (order.dir !== "asc" && order.dir !== "desc") throw new Error(`[resolver] unknown comparison order direction: ${JSON.stringify(order.dir)}`);
41
+ return {
42
+ column: sql.raw("CAST(COALESCE(c.clicks, 0) AS DOUBLE) - CAST(COALESCE(p.clicks, 0) AS DOUBLE) as \"clicksChange\""),
43
+ orderBy: sql.raw(`ORDER BY clicksChange ${order.dir.toUpperCase()}`)
44
+ };
45
+ }
38
46
  function limitOffsetClause(state) {
39
47
  const rowLimit = Math.max(0, Math.floor(Number(state.rowLimit ?? 100)));
40
48
  const offset = state.startRow ? Math.max(0, Math.floor(Number(state.startRow))) : 0;
@@ -239,7 +247,7 @@ function buildTotalsSql(state, options) {
239
247
  const selectExprs = metrics.map((m) => sql`${adapter.metricSql(m, tableKey)} as ${aliasRaw(m)}`);
240
248
  return compileCollapsed(adapter, wherePredicates.length > 0 ? sql`SELECT ${joinComma(selectExprs)} FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT ${joinComma(selectExprs)} FROM ${table}`);
241
249
  }
242
- function resolveComparisonSQL(current, previous, options, comparisonFilter) {
250
+ function resolveComparisonSQL(current, previous, options, comparisonFilter, comparisonOrderBy) {
243
251
  const { adapter, siteId, searchType } = options;
244
252
  const comparisonPlan = buildComparisonPlan(current, previous, adapter.capabilities);
245
253
  const currentScope = buildScope(current, options);
@@ -271,7 +279,8 @@ function resolveComparisonSQL(current, previous, options, comparisonFilter) {
271
279
  if (groupByExprs.length > 0) previousCte = sql`${previousCte} GROUP BY ${joinComma(groupByExprs)}`;
272
280
  const joinOn = groupByDims.length > 0 ? sql.raw(groupByDims.map((d) => `c.${d.replace(/\W/g, "")} = p.${d.replace(/\W/g, "")}`).join(" AND ")) : sql.raw("1=1");
273
281
  const filterClause = comparisonFilter ? COMPARISON_FILTER_SQL[comparisonFilter] : sql.raw("");
274
- const orderSql = orderByClause(current, "");
282
+ const changeOrder = comparisonOrderBy ? changeOrderClause(comparisonOrderBy) : void 0;
283
+ const orderSql = changeOrder?.orderBy ?? orderByClause(current, "");
275
284
  const limitSql = limitOffsetClause(current);
276
285
  const outerCurrentCols = [];
277
286
  for (const d of groupByDims) {
@@ -282,6 +291,7 @@ function resolveComparisonSQL(current, previous, options, comparisonFilter) {
282
291
  outerCurrentCols.push(sql.raw("CAST(COALESCE(c.impressions, 0) AS DOUBLE) as \"impressions\""));
283
292
  outerCurrentCols.push(sql.raw("COALESCE(c.ctr, 0) as \"ctr\""));
284
293
  outerCurrentCols.push(sql.raw("COALESCE(c.position, 0) as \"position\""));
294
+ if (changeOrder) outerCurrentCols.push(changeOrder.column);
285
295
  const mainQuery = sql`WITH current AS (${currentCte}), previous AS (${previousCte}) SELECT ${joinComma(outerCurrentCols)}, COALESCE(CAST(p.clicks AS DOUBLE), 0) as "prevClicks", COALESCE(CAST(p.impressions AS DOUBLE), 0) as "prevImpressions", COALESCE(p.ctr, 0) as "prevCtr", COALESCE(p.position, 0) as "prevPosition" FROM current c FULL OUTER JOIN previous p ON ${joinOn} WHERE 1=1 ${filterClause} ${orderSql} ${limitSql}`;
286
296
  const firstGroupBy = groupByDims[0] ? groupByDims[0].replace(/\W/g, "") : "clicks";
287
297
  const countInnerSelect = groupByDims[0] ? sql.raw(`COALESCE(c.${firstGroupBy}, p.${firstGroupBy})`) : sql.raw(`c.${firstGroupBy}`);
@@ -1,4 +1,4 @@
1
- import { ComparisonFilter, ExtraQuery, ResolvedComparisonSQL, ResolvedSQL, ResolvedSQLOptimized, ResolverAdapter, ResolverOptions } from "./types.mjs";
1
+ import { ComparisonFilter, ComparisonOrderBy, ExtraQuery, ResolvedComparisonSQL, ResolvedSQL, ResolvedSQLOptimized, ResolverAdapter, ResolverOptions } from "./types.mjs";
2
2
  import { DIMENSION_SURFACES, DimensionBinding, DimensionSurface, LOGICAL_DATASETS, LogicalDataset, LogicalDatasetDefinition, UnresolvableDatasetError, assertDimensionsSupported, dimensionColumn, inferLogicalDataset, isDatasetResolvable, supportsDimensionOnSurface } from "./datasets.mjs";
3
3
  import { SqlFragments, SqlFragmentsConfig, createSqlFragments } from "./fragments.mjs";
4
4
  import { CreateResolverAdapterConfig, createResolverAdapter } from "./adapter.mjs";
@@ -9,4 +9,4 @@ import { RollupRowsReader, createRollupExtrasOverlay } from "./extras-overlay.mj
9
9
  import { dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, metricValue } from "./filter-utils.mjs";
10
10
  import { PgTableKey, R2SqlResolverAdapterOptions, ResolverAdapterOptions, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, pgResolverAdapter } from "./pg-adapter.mjs";
11
11
  import { AssertSchemaInSyncOptions, assertSchemaInSync } from "./schema-drift.mjs";
12
- export { type AssertSchemaInSyncOptions, type CanonicalQueryDimSource, type ComparisonFilter, type ComparisonQueryResult, type CreateResolverAdapterConfig, DIMENSION_SURFACES, type DimensionBinding, type DimensionSurface, type ExtraQuery, LOGICAL_DATASETS, type LogicalDataset, type LogicalDatasetDefinition, type OptimizedQueryResult, type PgTableKey, type PrimaryColumnarSource, QuerySourceCoverageError, type QuerySourceDecision, type QuerySourceFallback, type QuerySourceFallbackKind, type QuerySourceKind, type R2SqlResolverAdapterOptions, type ResolveExtraFn, type ResolvedComparisonSQL, type ResolvedSQL, type ResolvedSQLOptimized, type ResolverAdapter, type ResolverAdapterOptions, type ResolverOptions, type RollupRowsReader, type RunOptimizedQueryOptions, type RunQueryCtx, type RunSQLFn, type SqlFragments, type SqlFragmentsConfig, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
12
+ export { type AssertSchemaInSyncOptions, type CanonicalQueryDimSource, type ComparisonFilter, type ComparisonOrderBy, type ComparisonQueryResult, type CreateResolverAdapterConfig, DIMENSION_SURFACES, type DimensionBinding, type DimensionSurface, type ExtraQuery, LOGICAL_DATASETS, type LogicalDataset, type LogicalDatasetDefinition, type OptimizedQueryResult, type PgTableKey, type PrimaryColumnarSource, QuerySourceCoverageError, type QuerySourceDecision, type QuerySourceFallback, type QuerySourceFallbackKind, type QuerySourceKind, type R2SqlResolverAdapterOptions, type ResolveExtraFn, type ResolvedComparisonSQL, type ResolvedSQL, type ResolvedSQLOptimized, type ResolverAdapter, type ResolverAdapterOptions, type ResolverOptions, type RollupRowsReader, type RunOptimizedQueryOptions, type RunQueryCtx, type RunSQLFn, type SqlFragments, type SqlFragmentsConfig, UnresolvableDatasetError, assertDimensionsSupported, assertSchemaInSync, buildExtrasQueries, buildTotalsSql, canonicalRollupCovers, createIcebergResolverAdapter, createParquetResolverAdapter, createR2SqlResolverAdapter, createResolverAdapter, createRollupExtrasOverlay, createSqlFragments, dimensionColumn, dimensionValue, getDimensionFilters, getFilterDimensions, getInternalFilters, inferLogicalDataset, isDatasetResolvable, matchesDimensionFilter, matchesMetricFilter, matchesTopLevelPage, mergeExtras, metricValue, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized, runComparisonQuery, runOptimizedQuery, supportsDimensionOnSurface };
@@ -29,6 +29,15 @@ export interface ResolverAdapter<TableKey extends string = string> {
29
29
  };
30
30
  }
31
31
  export type ComparisonFilter = 'new' | 'lost' | 'improving' | 'declining';
32
+ /**
33
+ * Order for a comparison read, applied to the joined rows before the row cap.
34
+ * `clicksChange` is current clicks minus previous clicks, so `asc` puts the
35
+ * biggest losers first and `desc` the biggest gainers.
36
+ */
37
+ export interface ComparisonOrderBy {
38
+ column: 'clicksChange';
39
+ dir: 'asc' | 'desc';
40
+ }
32
41
  export interface ResolverOptions<TableKey extends string = string> {
33
42
  adapter: ResolverAdapter<TableKey>;
34
43
  /** Optional site scope. Required for multi-tenant D1; omitted for parquet. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/engine",
3
3
  "type": "module",
4
- "version": "5.6.0",
4
+ "version": "5.7.1",
5
5
  "description": "Append-only Parquet/DuckDB storage engine + planner + adapters for the gscdump pipeline. Node + edge runtimes; opt-in heavy peers.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -189,10 +189,10 @@
189
189
  }
190
190
  },
191
191
  "dependencies": {
192
- "@gscdump/contracts": "^5.6.0",
193
- "@gscdump/lakehouse": "^5.6.0",
192
+ "@gscdump/contracts": "^5.7.1",
193
+ "@gscdump/lakehouse": "^5.7.1",
194
194
  "drizzle-orm": "1.0.0-rc.4",
195
- "gscdump": "^5.6.0",
195
+ "gscdump": "^5.7.1",
196
196
  "pathe": "^2.0.3",
197
197
  "proper-lockfile": "^4.1.2"
198
198
  },