@gscdump/engine 0.7.1 → 0.7.3
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.
- package/README.md +1 -1
- package/dist/_chunks/analysis-types.d.mts +47 -0
- package/dist/_chunks/contracts.d.mts +1 -0
- package/dist/_chunks/dispatch.mjs +75 -0
- package/dist/_chunks/registry.d.mts +92 -0
- package/dist/_chunks/resolver.mjs +91 -0
- package/dist/_chunks/source-types.d.mts +31 -0
- package/dist/analysis-types.d.mts +2 -0
- package/dist/analysis-types.mjs +7 -0
- package/dist/analyzer/index.d.mts +59 -0
- package/dist/analyzer/index.mjs +104 -0
- package/dist/contracts.d.mts +1 -1
- package/dist/period/index.d.mts +57 -0
- package/dist/period/index.mjs +150 -0
- package/dist/resolver/index.d.mts +1 -27
- package/dist/resolver/index.mjs +1 -89
- package/dist/scope.d.mts +3 -3
- package/dist/source/index.d.mts +78 -0
- package/dist/source/index.mjs +113 -0
- package/package.json +64 -27
- package/dist/rollups.d.mts +0 -162
- package/dist/rollups.mjs +0 -346
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ Optional peers (install only what your runtime needs):
|
|
|
61
61
|
- [`gscdump`](../gscdump) — REST client + query builder (edge-safe peer dep).
|
|
62
62
|
- [`@gscdump/analysis`](../analysis) — analyzers; consumes `StorageEngine` via `createEngine` factories.
|
|
63
63
|
- [`@gscdump/engine-duckdb-node`](../engine-duckdb-node) — Node DuckDB analyzer adapter.
|
|
64
|
-
- [`@gscdump/engine-wasm`](../engine-wasm) — DuckDB-WASM browser adapter.
|
|
64
|
+
- [`@gscdump/engine-duckdb-wasm`](../engine-duckdb-wasm) — DuckDB-WASM browser adapter.
|
|
65
65
|
- [`@gscdump/engine-sqlite`](../engine-sqlite) — SQLite / D1 adapter.
|
|
66
66
|
- [`@gscdump/cli`](../cli) — CLI wrapping engine + analysis.
|
|
67
67
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BuilderState } from "gscdump/query";
|
|
2
|
+
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';
|
|
3
|
+
interface AnalysisParams {
|
|
4
|
+
type: AnalysisTool;
|
|
5
|
+
startDate?: string;
|
|
6
|
+
endDate?: string;
|
|
7
|
+
prevStartDate?: string;
|
|
8
|
+
prevEndDate?: string;
|
|
9
|
+
brandTerms?: string[];
|
|
10
|
+
limit?: number;
|
|
11
|
+
offset?: number;
|
|
12
|
+
/** Sort column. Each analyzer enforces its own whitelist. */
|
|
13
|
+
sortBy?: string;
|
|
14
|
+
/** Sort direction. Default per-analyzer. */
|
|
15
|
+
sortDir?: 'asc' | 'desc';
|
|
16
|
+
minPosition?: number;
|
|
17
|
+
maxPosition?: number;
|
|
18
|
+
minImpressions?: number;
|
|
19
|
+
maxCtr?: number;
|
|
20
|
+
minPages?: number;
|
|
21
|
+
maxPositionSpread?: number;
|
|
22
|
+
minClusterSize?: number;
|
|
23
|
+
clusterBy?: 'prefix' | 'intent' | 'both';
|
|
24
|
+
dimension?: 'pages' | 'keywords';
|
|
25
|
+
topN?: number;
|
|
26
|
+
metric?: 'clicks' | 'impressions';
|
|
27
|
+
changeThreshold?: number;
|
|
28
|
+
minPreviousClicks?: number;
|
|
29
|
+
threshold?: number;
|
|
30
|
+
weeks?: number;
|
|
31
|
+
minWeeksWithData?: number;
|
|
32
|
+
/** content-velocity lookback window in days (max 365, default 90). */
|
|
33
|
+
days?: number;
|
|
34
|
+
/** data-query / data-detail primary BuilderState. */
|
|
35
|
+
q?: BuilderState;
|
|
36
|
+
/** data-query / data-detail optional comparison-period BuilderState. */
|
|
37
|
+
qc?: BuilderState;
|
|
38
|
+
/** data-query comparison filter applied to joined current/previous rows. */
|
|
39
|
+
comparisonFilter?: 'new' | 'lost' | 'improving' | 'declining';
|
|
40
|
+
}
|
|
41
|
+
interface AnalysisResult {
|
|
42
|
+
results: Record<string, unknown>[];
|
|
43
|
+
meta: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** Coerce arbitrary value (number, bigint, string, null) to number, defaulting to 0. */
|
|
46
|
+
declare function num(v: unknown): number;
|
|
47
|
+
export { num as i, AnalysisResult as n, AnalysisTool as r, AnalysisParams as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
var AnalyzerCapabilityError = class extends Error {
|
|
2
|
+
constructor(tool, missing) {
|
|
3
|
+
super(`analyzer "${tool}" requires capabilities [${missing.join(", ")}] not provided by source`);
|
|
4
|
+
this.tool = tool;
|
|
5
|
+
this.missing = missing;
|
|
6
|
+
this.name = "AnalyzerCapabilityError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function sourceCapabilities(source) {
|
|
10
|
+
const caps = /* @__PURE__ */ new Set();
|
|
11
|
+
if (source.executeSql) caps.add("executeSql");
|
|
12
|
+
if (source.capabilities.fileSets) caps.add("partitionedParquet");
|
|
13
|
+
if (source.capabilities.regex) caps.add("regex");
|
|
14
|
+
if (source.capabilities.windowTotals) caps.add("windowTotals");
|
|
15
|
+
if (source.capabilities.comparisonJoin) caps.add("comparisonJoin");
|
|
16
|
+
if (source.capabilities.attachedTables) caps.add("attachedTables");
|
|
17
|
+
return caps;
|
|
18
|
+
}
|
|
19
|
+
function assertSatisfies(analyzer, caps) {
|
|
20
|
+
const missing = analyzer.requires.filter((c) => !caps.has(c));
|
|
21
|
+
if (missing.length > 0) throw new AnalyzerCapabilityError(analyzer.id, missing);
|
|
22
|
+
}
|
|
23
|
+
async function runAnalyzerFromSource(source, params, registry) {
|
|
24
|
+
const caps = sourceCapabilities(source);
|
|
25
|
+
const analyzer = registry.resolveAnalyzer(params.type, caps.has("executeSql") || caps.has("attachedTables"));
|
|
26
|
+
if (!analyzer) throw new AnalyzerCapabilityError(params.type, ["executeSql"]);
|
|
27
|
+
assertSatisfies(analyzer, caps);
|
|
28
|
+
const plan = analyzer.build(params);
|
|
29
|
+
if (plan.kind === "rows") return runRowsPlanAgainstSource(source, analyzer, plan, params);
|
|
30
|
+
return runSqlPlanAgainstSource(source, analyzer, plan, params);
|
|
31
|
+
}
|
|
32
|
+
async function runRowsPlanAgainstSource(source, analyzer, plan, params) {
|
|
33
|
+
const entries = Object.entries(plan.queries);
|
|
34
|
+
const resolved = await Promise.all(entries.map(async ([k, q]) => [k, await source.queryRows(q.state)]));
|
|
35
|
+
const rowMap = Object.fromEntries(resolved);
|
|
36
|
+
const { results, meta } = analyzer.reduce(rowMap, { params });
|
|
37
|
+
return {
|
|
38
|
+
results,
|
|
39
|
+
meta: {
|
|
40
|
+
tool: params.type,
|
|
41
|
+
...meta
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function fileSetsFor(plan) {
|
|
46
|
+
const fileSets = { FILES: plan.current };
|
|
47
|
+
if (plan.previous) fileSets.FILES_PREV = plan.previous;
|
|
48
|
+
if (plan.extraFiles) for (const [key, fs] of Object.entries(plan.extraFiles)) fileSets[`FILES_${key}`] = fs;
|
|
49
|
+
return fileSets;
|
|
50
|
+
}
|
|
51
|
+
async function runSqlPlanAgainstSource(source, analyzer, plan, params) {
|
|
52
|
+
if (!source.executeSql) throw new AnalyzerCapabilityError(analyzer.id, ["executeSql"]);
|
|
53
|
+
if (plan.requiresAttachedTables && !source.capabilities.attachedTables) throw new AnalyzerCapabilityError(analyzer.id, ["attachedTables"]);
|
|
54
|
+
const fileSets = source.capabilities.fileSets ? fileSetsFor(plan) : void 0;
|
|
55
|
+
const rows = await source.executeSql(plan.sql, plan.params, fileSets ? { fileSets } : void 0);
|
|
56
|
+
const extras = {};
|
|
57
|
+
if (plan.extraQueries) for (const q of plan.extraQueries) {
|
|
58
|
+
const extraRows = await source.executeSql(q.sql, q.params, fileSets ? { fileSets } : void 0);
|
|
59
|
+
extras[q.name] = extraRows;
|
|
60
|
+
}
|
|
61
|
+
const { results, meta } = analyzer.reduce(rows, {
|
|
62
|
+
params,
|
|
63
|
+
extras
|
|
64
|
+
});
|
|
65
|
+
const sourceMeta = source.capabilities.localSource ? { source: "local" } : source.capabilities.attachedTables ? { source: "browser" } : {};
|
|
66
|
+
return {
|
|
67
|
+
results,
|
|
68
|
+
meta: {
|
|
69
|
+
tool: params.type,
|
|
70
|
+
...sourceMeta,
|
|
71
|
+
...meta
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export { runAnalyzerFromSource as n, AnalyzerCapabilityError as t };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { w as Row } from "./storage.mjs";
|
|
2
|
+
import { t as AnalysisParams } from "./analysis-types.mjs";
|
|
3
|
+
import { r as FileSet } from "./source-types.mjs";
|
|
4
|
+
import { BuilderState } from "gscdump/query";
|
|
5
|
+
/**
|
|
6
|
+
* Capabilities a Plan may require of its host. A dispatcher matches these
|
|
7
|
+
* against a source's declared capabilities and rejects mismatches.
|
|
8
|
+
*/
|
|
9
|
+
type Capability = 'executeSql' | 'partitionedParquet' | 'attachedTables' | 'regex' | 'windowTotals' | 'comparisonJoin';
|
|
10
|
+
interface SqlExtraQuery {
|
|
11
|
+
name: string;
|
|
12
|
+
sql: string;
|
|
13
|
+
params: unknown[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SQL-native plan: SQL string + placeholders, with optional extra file sets
|
|
17
|
+
* and follow-up queries.
|
|
18
|
+
*/
|
|
19
|
+
interface SqlPlan {
|
|
20
|
+
kind: 'sql';
|
|
21
|
+
sql: string;
|
|
22
|
+
params: unknown[];
|
|
23
|
+
current: FileSet;
|
|
24
|
+
previous?: FileSet;
|
|
25
|
+
extraFiles?: Record<string, FileSet>;
|
|
26
|
+
extraQueries?: SqlExtraQuery[];
|
|
27
|
+
/** Emits direct table refs (browser-only). Dispatcher rejects for manifest path. */
|
|
28
|
+
requiresAttachedTables?: boolean;
|
|
29
|
+
}
|
|
30
|
+
interface TypedRowQuery<T extends Row = Row> {
|
|
31
|
+
state: BuilderState;
|
|
32
|
+
/** Optional type tag for downstream narrowing. */
|
|
33
|
+
rowType?: (row: Row) => T;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Row-queries plan: a named set of typed `BuilderState` queries. A portable
|
|
37
|
+
* dispatcher runs each against a source's `queryRows` and hands the row
|
|
38
|
+
* collection to `reduce`.
|
|
39
|
+
*/
|
|
40
|
+
interface RowQueriesPlan {
|
|
41
|
+
kind: 'rows';
|
|
42
|
+
queries: Record<string, TypedRowQuery>;
|
|
43
|
+
}
|
|
44
|
+
type Plan = SqlPlan | RowQueriesPlan;
|
|
45
|
+
interface ReduceContext<TRow extends Row = Row> {
|
|
46
|
+
params: AnalysisParams;
|
|
47
|
+
/** Extra SQL-query results keyed by `SqlExtraQuery.name`. */
|
|
48
|
+
extras?: Record<string, TRow[]>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Unified analyzer contract. `TRow` lets authors narrow from the default
|
|
52
|
+
* `Row = Record<string, unknown>` to a typed row shape (e.g. `KeywordRow`)
|
|
53
|
+
* when their reducer assumes specific columns exist — catches drift between
|
|
54
|
+
* `build` (SELECT list) and `reduce` (column access) at compile time.
|
|
55
|
+
*/
|
|
56
|
+
interface Analyzer<P extends AnalysisParams = AnalysisParams, R = unknown, TRow extends Row = Row> {
|
|
57
|
+
/** Stable tool id (e.g. `striking-distance`, `opportunity`). */
|
|
58
|
+
id: string;
|
|
59
|
+
/** Capabilities a host source must provide. */
|
|
60
|
+
requires: readonly Capability[];
|
|
61
|
+
/** Pure: params → plan. Snapshot-testable. */
|
|
62
|
+
build: (params: P) => Plan;
|
|
63
|
+
/** Pure: rows + context → typed result + meta. */
|
|
64
|
+
reduce: (rows: TRow[] | Record<string, TRow[]>, ctx: ReduceContext<TRow>) => {
|
|
65
|
+
results: R;
|
|
66
|
+
meta?: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
interface AnalyzerVariants {
|
|
70
|
+
sql?: Analyzer;
|
|
71
|
+
rows?: Analyzer;
|
|
72
|
+
}
|
|
73
|
+
interface AnalyzerRegistryInit {
|
|
74
|
+
rows?: readonly Analyzer[];
|
|
75
|
+
sql?: readonly Analyzer[];
|
|
76
|
+
}
|
|
77
|
+
interface AnalyzerRegistry {
|
|
78
|
+
listAnalyzerIds: () => readonly string[];
|
|
79
|
+
getAnalyzerVariants: (id: string) => AnalyzerVariants | undefined;
|
|
80
|
+
resolveAnalyzer: (id: string, sourceSupportsSql: boolean) => Analyzer | undefined;
|
|
81
|
+
listAnalyzersFor: (sourceSupportsSql: boolean) => readonly Analyzer[];
|
|
82
|
+
listAnalyzerIdsFor: (source: {
|
|
83
|
+
executeSql?: unknown;
|
|
84
|
+
}) => readonly string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build an immutable registry from collections of row / SQL analyzers.
|
|
88
|
+
* No global state; call this once per logical use (typically at startup
|
|
89
|
+
* or per-request in a worker).
|
|
90
|
+
*/
|
|
91
|
+
declare function createAnalyzerRegistry(init?: AnalyzerRegistryInit): AnalyzerRegistry;
|
|
92
|
+
export { Analyzer as a, ReduceContext as c, SqlPlan as d, TypedRowQuery as f, createAnalyzerRegistry as i, RowQueriesPlan as l, AnalyzerRegistryInit as n, Capability as o, AnalyzerVariants as r, Plan as s, AnalyzerRegistry as t, SqlExtraQuery as u };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { t as SCHEMAS } from "./schema.mjs";
|
|
2
|
+
import { _ as resolveToSQL } from "./pg-adapter.mjs";
|
|
3
|
+
import { normalizeUrl } from "gscdump/normalize";
|
|
4
|
+
function createSqlQuerySource(options) {
|
|
5
|
+
const { name, adapter, execute, siteId, extraCapabilities } = options;
|
|
6
|
+
return {
|
|
7
|
+
name,
|
|
8
|
+
capabilities: {
|
|
9
|
+
...adapter.capabilities,
|
|
10
|
+
...extraCapabilities
|
|
11
|
+
},
|
|
12
|
+
async queryRows(state) {
|
|
13
|
+
const resolved = resolveToSQL(state, {
|
|
14
|
+
adapter,
|
|
15
|
+
siteId
|
|
16
|
+
});
|
|
17
|
+
return execute(resolved.sql, resolved.params);
|
|
18
|
+
},
|
|
19
|
+
executeSql(sql, params) {
|
|
20
|
+
return execute(sql, params ?? []);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function collectInternalFilters(filter) {
|
|
25
|
+
if (!filter || !("_filters" in filter)) return [];
|
|
26
|
+
const flat = filter._filters;
|
|
27
|
+
const nested = filter._nestedGroups?.flatMap((group) => collectInternalFilters(group)) ?? [];
|
|
28
|
+
return [...flat, ...nested];
|
|
29
|
+
}
|
|
30
|
+
function getInternalFilters(filter) {
|
|
31
|
+
return collectInternalFilters(filter);
|
|
32
|
+
}
|
|
33
|
+
function getDimensionFilters(filter, isMetricDimension) {
|
|
34
|
+
return collectInternalFilters(filter).filter((f) => f.dimension !== "date" && !isMetricDimension(f.dimension) && f.operator !== "topLevel" && !f.operator.startsWith("metric"));
|
|
35
|
+
}
|
|
36
|
+
function getFilterDimensions(filter, isMetricDimension) {
|
|
37
|
+
return getDimensionFilters(filter, isMetricDimension).map((f) => f.dimension);
|
|
38
|
+
}
|
|
39
|
+
function metricValue(row, metric) {
|
|
40
|
+
const value = row[metric];
|
|
41
|
+
if (typeof value === "number") return value;
|
|
42
|
+
if (typeof value === "bigint") return Number(value);
|
|
43
|
+
if (value == null) return 0;
|
|
44
|
+
return Number(value);
|
|
45
|
+
}
|
|
46
|
+
function dimensionValue(row, dimension) {
|
|
47
|
+
const value = row[dimension];
|
|
48
|
+
return value == null ? "" : String(value);
|
|
49
|
+
}
|
|
50
|
+
function matchesDimensionFilter(row, filter) {
|
|
51
|
+
const raw = dimensionValue(row, filter.dimension);
|
|
52
|
+
const value = filter.dimension === "page" ? normalizeUrl(raw) : raw;
|
|
53
|
+
switch (filter.operator) {
|
|
54
|
+
case "equals": return value === filter.expression;
|
|
55
|
+
case "notEquals": return value !== filter.expression;
|
|
56
|
+
case "contains": return raw.includes(filter.expression);
|
|
57
|
+
case "notContains": return !raw.includes(filter.expression);
|
|
58
|
+
case "includingRegex": return new RegExp(filter.expression).test(raw);
|
|
59
|
+
case "excludingRegex": return !new RegExp(filter.expression).test(raw);
|
|
60
|
+
default: return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function matchesMetricFilter(row, filter) {
|
|
64
|
+
const value = metricValue(row, filter.dimension);
|
|
65
|
+
const target = Number(filter.expression);
|
|
66
|
+
switch (filter.operator) {
|
|
67
|
+
case "metricGte": return value >= target;
|
|
68
|
+
case "metricGt": return value > target;
|
|
69
|
+
case "metricLte": return value <= target;
|
|
70
|
+
case "metricLt": return value < target;
|
|
71
|
+
case "metricBetween": return value >= target && value <= Number(filter.expression2);
|
|
72
|
+
default: return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function matchesTopLevelPage(row) {
|
|
76
|
+
return (normalizeUrl(dimensionValue(row, "page")).match(/\//g)?.length ?? 0) <= 1;
|
|
77
|
+
}
|
|
78
|
+
function assertSchemaInSync(options) {
|
|
79
|
+
const { label, schema, tableKeyToName, mode } = options;
|
|
80
|
+
for (const [key, table] of Object.entries(schema)) {
|
|
81
|
+
const sourceCols = SCHEMAS[tableKeyToName(key)].columns.map((c) => c.name).sort();
|
|
82
|
+
const drizzleCols = Object.keys(table[Symbol.for("drizzle:Columns")] ?? {}).sort();
|
|
83
|
+
const missing = sourceCols.filter((c) => !drizzleCols.includes(c));
|
|
84
|
+
const extra = mode === "exact" ? drizzleCols.filter((c) => !sourceCols.includes(c)) : [];
|
|
85
|
+
if (missing.length > 0 || extra.length > 0) throw new Error(`${label} drizzle schema for '${key}' drifted from SCHEMAS. Missing: [${missing.join(", ")}]. Extra: [${extra.join(", ")}].`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isSqlQuerySource(s) {
|
|
89
|
+
return typeof s.executeSql === "function";
|
|
90
|
+
}
|
|
91
|
+
export { getFilterDimensions as a, matchesMetricFilter as c, createSqlQuerySource as d, getDimensionFilters as i, matchesTopLevelPage as l, assertSchemaInSync as n, getInternalFilters as o, dimensionValue as r, matchesDimensionFilter as s, isSqlQuerySource as t, metricValue as u };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PlannerCapabilities } from "gscdump/query/plan";
|
|
2
|
+
import { TableName } from "gscdump/contracts";
|
|
3
|
+
import { BuilderState } from "gscdump/query";
|
|
4
|
+
type QueryRow = Record<string, unknown>;
|
|
5
|
+
interface FileSet {
|
|
6
|
+
table: TableName;
|
|
7
|
+
partitions: string[];
|
|
8
|
+
}
|
|
9
|
+
interface ExecuteSqlOptions {
|
|
10
|
+
fileSets?: Record<string, FileSet>;
|
|
11
|
+
}
|
|
12
|
+
interface SourceCapabilities extends PlannerCapabilities {
|
|
13
|
+
attachedTables?: boolean;
|
|
14
|
+
fileSets?: boolean;
|
|
15
|
+
localSource?: boolean;
|
|
16
|
+
}
|
|
17
|
+
interface RowQuerySource {
|
|
18
|
+
name?: string;
|
|
19
|
+
capabilities: SourceCapabilities;
|
|
20
|
+
queryRows: (state: BuilderState) => Promise<QueryRow[]>;
|
|
21
|
+
readonly executeSql?: undefined;
|
|
22
|
+
}
|
|
23
|
+
interface SqlQuerySource {
|
|
24
|
+
name?: string;
|
|
25
|
+
capabilities: SourceCapabilities;
|
|
26
|
+
queryRows: (state: BuilderState) => Promise<QueryRow[]>;
|
|
27
|
+
executeSql: (sql: string, params?: unknown[], opts?: ExecuteSqlOptions) => Promise<QueryRow[]>;
|
|
28
|
+
}
|
|
29
|
+
type AnalysisQuerySource = RowQuerySource | SqlQuerySource;
|
|
30
|
+
declare function isSqlQuerySource(s: AnalysisQuerySource): s is SqlQuerySource;
|
|
31
|
+
export { RowQuerySource as a, isSqlQuerySource as c, QueryRow as i, ExecuteSqlOptions as n, SourceCapabilities as o, FileSet as r, SqlQuerySource as s, AnalysisQuerySource as t };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { n as AnalysisResult, t as AnalysisParams } from "../_chunks/analysis-types.mjs";
|
|
2
|
+
import { r as FileSet, t as AnalysisQuerySource } from "../_chunks/source-types.mjs";
|
|
3
|
+
import { a as Analyzer, c as ReduceContext, d as SqlPlan, f as TypedRowQuery, i as createAnalyzerRegistry, l as RowQueriesPlan, n as AnalyzerRegistryInit, o as Capability, r as AnalyzerVariants, s as Plan, t as AnalyzerRegistry, u as SqlExtraQuery } from "../_chunks/registry.mjs";
|
|
4
|
+
import { BuilderState } from "gscdump/query";
|
|
5
|
+
interface SqlPlanSpec {
|
|
6
|
+
sql: string;
|
|
7
|
+
params: unknown[];
|
|
8
|
+
current: FileSet;
|
|
9
|
+
previous?: FileSet;
|
|
10
|
+
extraFiles?: Record<string, FileSet>;
|
|
11
|
+
extraQueries?: SqlExtraQuery[];
|
|
12
|
+
requiresAttachedTables?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface ReduceCtx<InputRow> {
|
|
15
|
+
/** Extra SQL-query results keyed by `SqlExtraQuery.name` (SQL path only). */
|
|
16
|
+
extras?: Record<string, InputRow[]>;
|
|
17
|
+
}
|
|
18
|
+
type Reducer<Params, InputRow, Result> = (rows: InputRow[] | Record<string, InputRow[]>, params: Params, ctx: ReduceCtx<InputRow>) => {
|
|
19
|
+
results: Result;
|
|
20
|
+
meta?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
interface DefineAnalyzerOptions<Params extends AnalysisParams, InputRow, Result> {
|
|
23
|
+
id: string;
|
|
24
|
+
/**
|
|
25
|
+
* Shared reducer used by both SQL and row paths. Use this when the
|
|
26
|
+
* post-aggregation row count is small and filter/sort/derive can live in
|
|
27
|
+
* one place. Mutually exclusive with `reduceSql` / `reduceRows`.
|
|
28
|
+
*/
|
|
29
|
+
reduce?: Reducer<Params, InputRow, Result>;
|
|
30
|
+
/** SQL-only reducer. Required when `buildSql` is set without `reduce`. */
|
|
31
|
+
reduceSql?: Reducer<Params, InputRow, Result>;
|
|
32
|
+
/** Row-only reducer. Required when `buildRows` is set without `reduce`. */
|
|
33
|
+
reduceRows?: Reducer<Params, InputRow, Result>;
|
|
34
|
+
/** SQL plan builder. Omit if the analyzer has no SQL path. */
|
|
35
|
+
buildSql?: (params: Params) => SqlPlanSpec;
|
|
36
|
+
/** Row plan builder. Omit if the analyzer has no row path. */
|
|
37
|
+
buildRows?: (params: Params) => Record<string, BuilderState>;
|
|
38
|
+
/** Capabilities required by the SQL plan. Defaults to `['executeSql', 'partitionedParquet']`. */
|
|
39
|
+
sqlRequires?: readonly Capability[];
|
|
40
|
+
/** Capabilities required by the row plan. Defaults to `[]`. */
|
|
41
|
+
rowsRequires?: readonly Capability[];
|
|
42
|
+
}
|
|
43
|
+
interface DefinedAnalyzer {
|
|
44
|
+
id: string;
|
|
45
|
+
sql?: Analyzer;
|
|
46
|
+
rows?: Analyzer;
|
|
47
|
+
}
|
|
48
|
+
declare function defineAnalyzer<Params extends AnalysisParams, InputRow, Result>(opts: DefineAnalyzerOptions<Params, InputRow, Result>): DefinedAnalyzer;
|
|
49
|
+
declare class AnalyzerCapabilityError extends Error {
|
|
50
|
+
readonly tool: string;
|
|
51
|
+
readonly missing: readonly Capability[];
|
|
52
|
+
constructor(tool: string, missing: readonly Capability[]);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Run an analyzer against a generic `AnalysisQuerySource`. The registry is
|
|
56
|
+
* an explicit parameter — callers build one via `createAnalyzerRegistry`.
|
|
57
|
+
*/
|
|
58
|
+
declare function runAnalyzerFromSource(source: AnalysisQuerySource, params: AnalysisParams, registry: AnalyzerRegistry): Promise<AnalysisResult>;
|
|
59
|
+
export { type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerVariants, type Capability, type DefineAnalyzerOptions, type DefinedAnalyzer, type Plan, type ReduceContext, type ReduceCtx, type Reducer, type RowQueriesPlan, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type TypedRowQuery, createAnalyzerRegistry, defineAnalyzer, runAnalyzerFromSource };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { n as runAnalyzerFromSource, t as AnalyzerCapabilityError } from "../_chunks/dispatch.mjs";
|
|
2
|
+
const DEFAULT_SQL_REQUIRES = ["executeSql", "partitionedParquet"];
|
|
3
|
+
function defineAnalyzer(opts) {
|
|
4
|
+
const { id, reduce, reduceSql, reduceRows, buildSql, buildRows, sqlRequires = DEFAULT_SQL_REQUIRES, rowsRequires = [] } = opts;
|
|
5
|
+
const sqlReducer = reduceSql ?? reduce;
|
|
6
|
+
const rowsReducer = reduceRows ?? reduce;
|
|
7
|
+
if (buildSql && !sqlReducer) throw new Error(`defineAnalyzer(${id}): buildSql requires reduce or reduceSql`);
|
|
8
|
+
if (buildRows && !rowsReducer) throw new Error(`defineAnalyzer(${id}): buildRows requires reduce or reduceRows`);
|
|
9
|
+
const wrap = (fn) => (rows, params, ctx) => {
|
|
10
|
+
return fn(Array.isArray(rows) ? rows : pickSingle(rows) ?? rows, params, ctx);
|
|
11
|
+
};
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
sql: buildSql && sqlReducer ? {
|
|
15
|
+
id,
|
|
16
|
+
requires: sqlRequires,
|
|
17
|
+
build(params) {
|
|
18
|
+
const spec = buildSql(params);
|
|
19
|
+
return {
|
|
20
|
+
kind: "sql",
|
|
21
|
+
sql: spec.sql,
|
|
22
|
+
params: spec.params,
|
|
23
|
+
current: spec.current,
|
|
24
|
+
previous: spec.previous,
|
|
25
|
+
extraFiles: spec.extraFiles,
|
|
26
|
+
extraQueries: spec.extraQueries,
|
|
27
|
+
requiresAttachedTables: spec.requiresAttachedTables
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
reduce(rows, ctx) {
|
|
31
|
+
const { results, meta } = wrap(sqlReducer)(rows, ctx.params, { extras: ctx.extras });
|
|
32
|
+
return {
|
|
33
|
+
results,
|
|
34
|
+
meta
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
} : void 0,
|
|
38
|
+
rows: buildRows && rowsReducer ? {
|
|
39
|
+
id,
|
|
40
|
+
requires: rowsRequires,
|
|
41
|
+
build(params) {
|
|
42
|
+
const queries = buildRows(params);
|
|
43
|
+
return {
|
|
44
|
+
kind: "rows",
|
|
45
|
+
queries: Object.fromEntries(Object.entries(queries).map(([k, state]) => [k, { state }]))
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
reduce(rows, ctx) {
|
|
49
|
+
const { results, meta } = wrap(rowsReducer)(rows, ctx.params, {});
|
|
50
|
+
return {
|
|
51
|
+
results,
|
|
52
|
+
meta
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} : void 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function pickSingle(rows) {
|
|
59
|
+
const keys = Object.keys(rows);
|
|
60
|
+
return keys.length === 1 ? rows[keys[0]] : void 0;
|
|
61
|
+
}
|
|
62
|
+
function createAnalyzerRegistry(init = {}) {
|
|
63
|
+
const byId = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const a of init.rows ?? []) {
|
|
65
|
+
const entry = byId.get(a.id) ?? {};
|
|
66
|
+
entry.rows = a;
|
|
67
|
+
byId.set(a.id, entry);
|
|
68
|
+
}
|
|
69
|
+
for (const a of init.sql ?? []) {
|
|
70
|
+
const entry = byId.get(a.id) ?? {};
|
|
71
|
+
entry.sql = a;
|
|
72
|
+
byId.set(a.id, entry);
|
|
73
|
+
}
|
|
74
|
+
const listAnalyzerIds = () => [...byId.keys()].sort();
|
|
75
|
+
const getAnalyzerVariants = (id) => byId.get(id);
|
|
76
|
+
const resolveAnalyzer = (id, sourceSupportsSql) => {
|
|
77
|
+
const variants = byId.get(id);
|
|
78
|
+
if (!variants) return void 0;
|
|
79
|
+
if (sourceSupportsSql) return variants.sql ?? variants.rows;
|
|
80
|
+
return variants.rows;
|
|
81
|
+
};
|
|
82
|
+
const listAnalyzersFor = (sourceSupportsSql) => {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const id of listAnalyzerIds()) {
|
|
85
|
+
const a = resolveAnalyzer(id, sourceSupportsSql);
|
|
86
|
+
if (a) out.push(a);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
};
|
|
90
|
+
const listAnalyzerIdsFor = (source) => {
|
|
91
|
+
const sourceSupportsSql = typeof source.executeSql === "function";
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const id of listAnalyzerIds()) if (resolveAnalyzer(id, sourceSupportsSql)) out.push(id);
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
listAnalyzerIds,
|
|
98
|
+
getAnalyzerVariants,
|
|
99
|
+
resolveAnalyzer,
|
|
100
|
+
listAnalyzersFor,
|
|
101
|
+
listAnalyzerIdsFor
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export { AnalyzerCapabilityError, createAnalyzerRegistry, defineAnalyzer, runAnalyzerFromSource };
|
package/dist/contracts.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { A as SyncStateFilter, C as QueryResult, D as StorageEngine, E as SearchType, F as Watermark, I as WatermarkFilter, L as WatermarkScope, M as SyncStateScope, N as TableName, O as SyncState, P as TenantCtx, R as WriteCtx, S as QueryExecutor, T as RunSQLOptions, a as DataSource, b as QueryExecuteOptions, c as FileSetRef, d as LockScope, f as ManifestEntry, h as ParquetCodec, j as SyncStateKind, k as SyncStateDetail, l as GcCtx, m as ManifestStore, n as CompactionTier, o as EngineOptions, t as CodecCtx, u as ListLiveFilter, w as Row, x as QueryExecuteResult, y as QueryCtx, z as WriteResult } from "./_chunks/storage.mjs";
|
|
2
|
-
export {
|
|
2
|
+
export { CodecCtx, CompactionTier, DataSource, EngineOptions, FileSetRef, GcCtx, ListLiveFilter, LockScope, ManifestEntry, ManifestStore, ParquetCodec, QueryCtx, QueryExecuteOptions, QueryExecuteResult, QueryExecutor, QueryResult, Row, RunSQLOptions, SearchType, StorageEngine, SyncState, SyncStateDetail, SyncStateFilter, SyncStateKind, SyncStateScope, TableName, TenantCtx, Watermark, WatermarkFilter, WatermarkScope, WriteCtx, WriteResult };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { t as AnalysisParams } from "../_chunks/analysis-types.mjs";
|
|
2
|
+
type WindowPreset = 'last-7d' | 'last-28d' | 'last-30d' | 'last-90d' | 'last-180d' | 'last-365d' | 'mtd' | 'ytd' | 'custom';
|
|
3
|
+
type ComparisonMode = 'none' | 'prev-period' | 'yoy';
|
|
4
|
+
interface ResolveWindowOptions {
|
|
5
|
+
preset: WindowPreset;
|
|
6
|
+
comparison?: ComparisonMode;
|
|
7
|
+
anchor?: string;
|
|
8
|
+
start?: string;
|
|
9
|
+
end?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ResolvedWindow {
|
|
12
|
+
start: string;
|
|
13
|
+
end: string;
|
|
14
|
+
days: number;
|
|
15
|
+
comparison?: {
|
|
16
|
+
start: string;
|
|
17
|
+
end: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface AnalysisPeriod {
|
|
21
|
+
startDate: string;
|
|
22
|
+
endDate: string;
|
|
23
|
+
}
|
|
24
|
+
interface ComparisonPeriod {
|
|
25
|
+
current: AnalysisPeriod;
|
|
26
|
+
previous: AnalysisPeriod;
|
|
27
|
+
}
|
|
28
|
+
declare function defaultEndDate(): string;
|
|
29
|
+
declare function defaultStartDate(): string;
|
|
30
|
+
declare function periodOf(params: AnalysisParams): AnalysisPeriod;
|
|
31
|
+
declare function comparisonOf(params: AnalysisParams): ComparisonPeriod;
|
|
32
|
+
declare function resolveWindow(opts: ResolveWindowOptions): ResolvedWindow;
|
|
33
|
+
/** Convert a ResolvedWindow into the AnalysisPeriod / ComparisonPeriod shape. */
|
|
34
|
+
declare function windowToPeriod(w: ResolvedWindow): AnalysisPeriod;
|
|
35
|
+
declare function windowToComparisonPeriod(w: ResolvedWindow): ComparisonPeriod | undefined;
|
|
36
|
+
interface PadTimeseriesOptions<T> {
|
|
37
|
+
/** ISO date (YYYY-MM-DD), inclusive lower bound. */
|
|
38
|
+
startDate: string;
|
|
39
|
+
/** ISO date (YYYY-MM-DD), inclusive upper bound. */
|
|
40
|
+
endDate: string;
|
|
41
|
+
/**
|
|
42
|
+
* Row to insert for missing dates. Defaults to `{ clicks: 0, impressions: 0, ctr: 0, position: 0 }`.
|
|
43
|
+
* The `date` field is set automatically.
|
|
44
|
+
*/
|
|
45
|
+
fill?: Omit<T, 'date'>;
|
|
46
|
+
/** Row-field that carries the ISO date. Defaults to `date`. */
|
|
47
|
+
dateKey?: string;
|
|
48
|
+
}
|
|
49
|
+
type DateRowShape = Record<string, unknown> & {
|
|
50
|
+
date?: unknown;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Pad rows so every calendar day in `[startDate, endDate]` appears at least
|
|
54
|
+
* once. Existing dates keep all their rows (grouped timeseries safe).
|
|
55
|
+
*/
|
|
56
|
+
declare function padTimeseries<T extends DateRowShape = DateRowShape>(rows: readonly T[], options: PadTimeseriesOptions<T>): T[];
|
|
57
|
+
export { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, defaultEndDate, defaultStartDate, padTimeseries, periodOf, resolveWindow, windowToComparisonPeriod, windowToPeriod };
|