@gscdump/engine 0.6.1 → 0.6.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/dist/_chunks/compiler.mjs +288 -0
- package/dist/_chunks/duckdb.d.mts +26 -0
- package/dist/_chunks/engine.mjs +578 -0
- package/dist/_chunks/pg-adapter.mjs +676 -0
- package/dist/_chunks/planner.d.mts +15 -0
- package/dist/_chunks/schema.d.mts +1258 -0
- package/dist/_chunks/schema.mjs +139 -0
- package/dist/_chunks/storage.d.mts +476 -0
- package/dist/_chunks/storage.mjs +39 -0
- package/dist/_chunks/types.d.mts +53 -0
- package/dist/adapters/duckdb-node.d.mts +1 -13
- package/dist/adapters/duckdb-node.mjs +1 -7
- package/dist/adapters/filesystem.d.mts +1 -193
- package/dist/adapters/filesystem.mjs +2 -9
- package/dist/adapters/http.d.mts +1 -193
- package/dist/adapters/http.mjs +1 -5
- package/dist/adapters/hyparquet.d.mts +6 -83
- package/dist/adapters/hyparquet.mjs +1 -105
- package/dist/adapters/inspection-sqlite-browser.d.mts +1 -7
- package/dist/adapters/inspection-sqlite-node.d.mts +1 -7
- package/dist/adapters/inspection-sqlite-node.mjs +1 -1
- package/dist/adapters/node-harness.d.mts +3 -306
- package/dist/adapters/node-harness.mjs +4 -1866
- package/dist/adapters/r2-manifest.d.mts +4 -149
- package/dist/adapters/r2-manifest.mjs +1 -8
- package/dist/adapters/r2.d.mts +1 -47
- package/dist/contracts.d.mts +1 -435
- package/dist/entities.d.mts +1 -47
- package/dist/index.d.mts +8 -1844
- package/dist/index.mjs +8 -1962
- package/dist/ingest.d.mts +1 -1
- package/dist/planner.d.mts +3 -16
- package/dist/planner.mjs +1 -320
- package/dist/resolver/index.d.mts +3 -51
- package/dist/resolver/index.mjs +2 -780
- package/dist/rollups.d.mts +6 -51
- package/dist/rollups.mjs +2 -209
- package/dist/schema.d.mts +2 -1258
- package/dist/schema.mjs +1 -138
- package/package.json +2 -2
package/dist/resolver/index.mjs
CHANGED
|
@@ -1,632 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { PgDialect, date, doublePrecision, getTableConfig, integer, pgTable, varchar } from "drizzle-orm/pg-core";
|
|
4
|
-
import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { t as SCHEMAS } from "../_chunks/schema.mjs";
|
|
2
|
+
import { _ as resolveToSQL, a as createResolverAdapter, c as LOGICAL_DATASETS, d as inferLogicalDataset, f as supportsDimensionOnSurface, g as resolveComparisonSQL, h as mergeExtras, i as compileSqlite, l as assertDimensionsSupported, m as buildTotalsSql, n as pgResolverAdapter, o as createSqlFragments, p as buildExtrasQueries, r as compilePg, s as DIMENSION_SURFACES, t as createParquetResolverAdapter, u as dimensionColumn, v as resolveToSQLOptimized } from "../_chunks/pg-adapter.mjs";
|
|
5
3
|
import { normalizeUrl } from "gscdump/normalize";
|
|
6
|
-
function escapeLike(value) {
|
|
7
|
-
return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
8
|
-
}
|
|
9
|
-
const DIMENSION_SURFACES = {
|
|
10
|
-
page: ["api", "stored"],
|
|
11
|
-
query: ["api", "stored"],
|
|
12
|
-
queryCanonical: ["stored", "derived"],
|
|
13
|
-
country: ["api", "stored"],
|
|
14
|
-
device: ["api", "stored"],
|
|
15
|
-
searchAppearance: ["api", "stored"],
|
|
16
|
-
date: ["api", "stored"]
|
|
17
|
-
};
|
|
18
|
-
const LOGICAL_DATASETS = {
|
|
19
|
-
pages: { dimensions: {
|
|
20
|
-
page: {
|
|
21
|
-
column: "url",
|
|
22
|
-
surfaces: ["api", "stored"]
|
|
23
|
-
},
|
|
24
|
-
date: {
|
|
25
|
-
column: "date",
|
|
26
|
-
surfaces: ["api", "stored"]
|
|
27
|
-
}
|
|
28
|
-
} },
|
|
29
|
-
keywords: { dimensions: {
|
|
30
|
-
query: {
|
|
31
|
-
column: "query",
|
|
32
|
-
surfaces: ["api", "stored"]
|
|
33
|
-
},
|
|
34
|
-
queryCanonical: {
|
|
35
|
-
column: "query_canonical",
|
|
36
|
-
surfaces: ["stored", "derived"]
|
|
37
|
-
},
|
|
38
|
-
date: {
|
|
39
|
-
column: "date",
|
|
40
|
-
surfaces: ["api", "stored"]
|
|
41
|
-
}
|
|
42
|
-
} },
|
|
43
|
-
page_keywords: { dimensions: {
|
|
44
|
-
page: {
|
|
45
|
-
column: "url",
|
|
46
|
-
surfaces: ["api", "stored"]
|
|
47
|
-
},
|
|
48
|
-
query: {
|
|
49
|
-
column: "query",
|
|
50
|
-
surfaces: ["api", "stored"]
|
|
51
|
-
},
|
|
52
|
-
queryCanonical: {
|
|
53
|
-
column: "query_canonical",
|
|
54
|
-
surfaces: ["stored", "derived"]
|
|
55
|
-
},
|
|
56
|
-
date: {
|
|
57
|
-
column: "date",
|
|
58
|
-
surfaces: ["api", "stored"]
|
|
59
|
-
}
|
|
60
|
-
} },
|
|
61
|
-
countries: { dimensions: {
|
|
62
|
-
country: {
|
|
63
|
-
column: "country",
|
|
64
|
-
surfaces: ["api", "stored"]
|
|
65
|
-
},
|
|
66
|
-
date: {
|
|
67
|
-
column: "date",
|
|
68
|
-
surfaces: ["api", "stored"]
|
|
69
|
-
}
|
|
70
|
-
} },
|
|
71
|
-
devices: { dimensions: {
|
|
72
|
-
device: {
|
|
73
|
-
column: "device",
|
|
74
|
-
surfaces: ["api", "stored"]
|
|
75
|
-
},
|
|
76
|
-
date: {
|
|
77
|
-
column: "date",
|
|
78
|
-
surfaces: ["api", "stored"]
|
|
79
|
-
}
|
|
80
|
-
} },
|
|
81
|
-
search_appearance: { dimensions: {
|
|
82
|
-
searchAppearance: {
|
|
83
|
-
column: "searchAppearance",
|
|
84
|
-
surfaces: ["api", "stored"]
|
|
85
|
-
},
|
|
86
|
-
date: {
|
|
87
|
-
column: "date",
|
|
88
|
-
surfaces: ["api", "stored"]
|
|
89
|
-
}
|
|
90
|
-
} }
|
|
91
|
-
};
|
|
92
|
-
function inferLogicalDataset(dimensions, filterDims = []) {
|
|
93
|
-
const allDims = new Set([...dimensions, ...filterDims]);
|
|
94
|
-
const has = (d) => allDims.has(d);
|
|
95
|
-
if (has("searchAppearance")) return "search_appearance";
|
|
96
|
-
if (has("page") && (has("query") || has("queryCanonical"))) return "page_keywords";
|
|
97
|
-
if (has("query") || has("queryCanonical")) return "keywords";
|
|
98
|
-
if (has("page")) return "pages";
|
|
99
|
-
if (has("country")) return "countries";
|
|
100
|
-
if (has("device")) return "devices";
|
|
101
|
-
return "keywords";
|
|
102
|
-
}
|
|
103
|
-
function dimensionColumn(dim, dataset) {
|
|
104
|
-
return LOGICAL_DATASETS[dataset].dimensions[dim]?.column ?? dim;
|
|
105
|
-
}
|
|
106
|
-
function supportsDimensionOnSurface(dim, surface) {
|
|
107
|
-
return DIMENSION_SURFACES[dim].includes(surface);
|
|
108
|
-
}
|
|
109
|
-
function assertDimensionsSupported(dimensions, surface, context) {
|
|
110
|
-
const unsupported = dimensions.filter((dim) => !supportsDimensionOnSurface(dim, surface));
|
|
111
|
-
if (unsupported.length === 0) return;
|
|
112
|
-
throw new Error(`${context}: unsupported dimensions for ${surface}: ${unsupported.join(", ")}`);
|
|
113
|
-
}
|
|
114
|
-
const METRIC_NAMES = [
|
|
115
|
-
"clicks",
|
|
116
|
-
"impressions",
|
|
117
|
-
"ctr",
|
|
118
|
-
"position"
|
|
119
|
-
];
|
|
120
|
-
function defaultSqliteUrlToPathExpr(col) {
|
|
121
|
-
return `CASE WHEN ${col} LIKE 'http%' THEN CASE WHEN INSTR(SUBSTR(${col}, INSTR(${col}, '://') + 3), '/') > 0 THEN SUBSTR(${col}, INSTR(${col}, '://') + 2 + INSTR(SUBSTR(${col}, INSTR(${col}, '://') + 3), '/')) ELSE '/' END ELSE ${col} END`;
|
|
122
|
-
}
|
|
123
|
-
function buildDimensionColumnMap(datasetToTableKey) {
|
|
124
|
-
const entries = Object.entries(datasetToTableKey).map(([dataset, tableKey]) => {
|
|
125
|
-
const dims = LOGICAL_DATASETS[dataset].dimensions;
|
|
126
|
-
return [tableKey, Object.fromEntries(Object.entries(dims).map(([dim, binding]) => [dim, binding?.column ?? dim]))];
|
|
127
|
-
});
|
|
128
|
-
return Object.fromEntries(entries);
|
|
129
|
-
}
|
|
130
|
-
function createSqlFragments(config) {
|
|
131
|
-
const { schema, datasetToTableKey, metricCast, regexPredicate, tableLabel, includeSiteId, urlToPathExpr: urlToPathExprOverride, tableRef: tableRefOverride } = config;
|
|
132
|
-
const DIM_COLUMN_MAP = buildDimensionColumnMap(datasetToTableKey);
|
|
133
|
-
function isMetricDimension(dim) {
|
|
134
|
-
return METRIC_NAMES.includes(dim);
|
|
135
|
-
}
|
|
136
|
-
function dimColumn(dim, table) {
|
|
137
|
-
return DIM_COLUMN_MAP[table]?.[dim] ?? dim;
|
|
138
|
-
}
|
|
139
|
-
function tableKeyForDataset(dataset) {
|
|
140
|
-
return datasetToTableKey[dataset];
|
|
141
|
-
}
|
|
142
|
-
function inferTable(dimensions, filterDims = []) {
|
|
143
|
-
return tableKeyForDataset(inferLogicalDataset(dimensions, filterDims));
|
|
144
|
-
}
|
|
145
|
-
const urlToPathExpr = urlToPathExprOverride ?? defaultSqliteUrlToPathExpr;
|
|
146
|
-
function colRef(tableKey, colName) {
|
|
147
|
-
const c = schema[tableKey][colName];
|
|
148
|
-
if (!c) throw new Error(`${tableLabel}: unknown column '${colName}' on ${tableKey}`);
|
|
149
|
-
return sql`${c}`;
|
|
150
|
-
}
|
|
151
|
-
function tableRef(tableKey) {
|
|
152
|
-
if (tableRefOverride) return tableRefOverride(tableKey);
|
|
153
|
-
return sql`${schema[tableKey]}`;
|
|
154
|
-
}
|
|
155
|
-
function dateColRef(tableKey) {
|
|
156
|
-
return colRef(tableKey, "date");
|
|
157
|
-
}
|
|
158
|
-
function siteIdColRef(tableKey) {
|
|
159
|
-
return colRef(tableKey, "site_id");
|
|
160
|
-
}
|
|
161
|
-
function dimExprSql(dim, tableKey) {
|
|
162
|
-
const colName = dimColumn(dim, tableKey);
|
|
163
|
-
if (dim === "page") return sql.raw(urlToPathExpr(colName));
|
|
164
|
-
return colRef(tableKey, colName);
|
|
165
|
-
}
|
|
166
|
-
function metricSql(metric, tableKey) {
|
|
167
|
-
const t = schema[tableKey];
|
|
168
|
-
switch (metric) {
|
|
169
|
-
case "clicks": return sql`CAST(SUM(${t.clicks}) AS ${sql.raw(metricCast)})`;
|
|
170
|
-
case "impressions": return sql`CAST(SUM(${t.impressions}) AS ${sql.raw(metricCast)})`;
|
|
171
|
-
case "ctr": return sql`CAST(SUM(${t.clicks}) AS ${sql.raw(metricCast)}) / NULLIF(SUM(${t.impressions}), 0)`;
|
|
172
|
-
case "position": return sql`SUM(${t.sum_position}) / NULLIF(SUM(${t.impressions}), 0) + 1`;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function havingPredicates(filters, tableKey) {
|
|
176
|
-
const preds = [];
|
|
177
|
-
for (const f of filters) {
|
|
178
|
-
const metric = f.dimension;
|
|
179
|
-
if (!isMetricDimension(metric)) continue;
|
|
180
|
-
const expr = metricSql(metric, tableKey);
|
|
181
|
-
const v = Number(f.expression);
|
|
182
|
-
switch (f.operator) {
|
|
183
|
-
case "metricGte":
|
|
184
|
-
preds.push(sql`${expr} >= ${v}`);
|
|
185
|
-
break;
|
|
186
|
-
case "metricGt":
|
|
187
|
-
preds.push(sql`${expr} > ${v}`);
|
|
188
|
-
break;
|
|
189
|
-
case "metricLte":
|
|
190
|
-
preds.push(sql`${expr} <= ${v}`);
|
|
191
|
-
break;
|
|
192
|
-
case "metricLt":
|
|
193
|
-
preds.push(sql`${expr} < ${v}`);
|
|
194
|
-
break;
|
|
195
|
-
case "metricBetween": {
|
|
196
|
-
const v2 = Number(f.expression2);
|
|
197
|
-
preds.push(sql`${expr} >= ${v} AND ${expr} <= ${v2}`);
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return preds;
|
|
203
|
-
}
|
|
204
|
-
function dimensionPredicates(filters, tableKey) {
|
|
205
|
-
const preds = [];
|
|
206
|
-
for (const f of filters) {
|
|
207
|
-
if (isMetricDimension(f.dimension)) continue;
|
|
208
|
-
if (f.dimension === "date") continue;
|
|
209
|
-
if (f.operator === "topLevel") continue;
|
|
210
|
-
const cRef = colRef(tableKey, dimColumn(f.dimension, tableKey));
|
|
211
|
-
const matchExpr = f.dimension === "page" ? dimExprSql(f.dimension, tableKey) : cRef;
|
|
212
|
-
switch (f.operator) {
|
|
213
|
-
case "equals":
|
|
214
|
-
preds.push(sql`${matchExpr} = ${f.expression}`);
|
|
215
|
-
break;
|
|
216
|
-
case "notEquals":
|
|
217
|
-
preds.push(sql`${matchExpr} != ${f.expression}`);
|
|
218
|
-
break;
|
|
219
|
-
case "contains":
|
|
220
|
-
preds.push(sql`${cRef} LIKE ${`%${escapeLike(f.expression)}%`} ESCAPE '\\'`);
|
|
221
|
-
break;
|
|
222
|
-
case "notContains":
|
|
223
|
-
preds.push(sql`${cRef} NOT LIKE ${`%${escapeLike(f.expression)}%`} ESCAPE '\\'`);
|
|
224
|
-
break;
|
|
225
|
-
case "includingRegex":
|
|
226
|
-
preds.push(regexPredicate(cRef, f.expression, false));
|
|
227
|
-
break;
|
|
228
|
-
case "excludingRegex":
|
|
229
|
-
preds.push(regexPredicate(cRef, f.expression, true));
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return preds;
|
|
234
|
-
}
|
|
235
|
-
function topLevelPredicate(filters, tableKey) {
|
|
236
|
-
if (!filters.some((f) => f.operator === "topLevel")) return void 0;
|
|
237
|
-
const pathExpr = dimExprSql("page", tableKey);
|
|
238
|
-
return sql`LENGTH(${pathExpr}) - LENGTH(REPLACE(${pathExpr}, '/', '')) <= 1`;
|
|
239
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
METRIC_NAMES,
|
|
242
|
-
DIM_COLUMN_MAP,
|
|
243
|
-
isMetricDimension,
|
|
244
|
-
tableKeyForDataset,
|
|
245
|
-
dimColumn,
|
|
246
|
-
inferTable,
|
|
247
|
-
urlToPathExpr,
|
|
248
|
-
colRef,
|
|
249
|
-
tableRef,
|
|
250
|
-
dateColRef,
|
|
251
|
-
siteIdColRef: includeSiteId ? siteIdColRef : void 0,
|
|
252
|
-
dimExprSql,
|
|
253
|
-
metricSql,
|
|
254
|
-
havingPredicates,
|
|
255
|
-
dimensionPredicates,
|
|
256
|
-
topLevelPredicate
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
function createResolverAdapter(config) {
|
|
260
|
-
const runtime = createSqlFragments(config);
|
|
261
|
-
return {
|
|
262
|
-
METRIC_NAMES: runtime.METRIC_NAMES,
|
|
263
|
-
capabilities: config.capabilities,
|
|
264
|
-
schema: config.schema,
|
|
265
|
-
tableKeyForDataset: runtime.tableKeyForDataset,
|
|
266
|
-
inferTable: runtime.inferTable,
|
|
267
|
-
dimColumn: runtime.dimColumn,
|
|
268
|
-
isMetricDimension: runtime.isMetricDimension,
|
|
269
|
-
tableRef: runtime.tableRef,
|
|
270
|
-
dateColRef: runtime.dateColRef,
|
|
271
|
-
urlToPathExpr: runtime.urlToPathExpr,
|
|
272
|
-
siteIdColRef: runtime.siteIdColRef,
|
|
273
|
-
dimExprSql: runtime.dimExprSql,
|
|
274
|
-
metricSql: runtime.metricSql,
|
|
275
|
-
dimensionPredicates: runtime.dimensionPredicates,
|
|
276
|
-
havingPredicates: runtime.havingPredicates,
|
|
277
|
-
topLevelPredicate: runtime.topLevelPredicate,
|
|
278
|
-
compile: config.compile
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const COMPARISON_FILTER_SQL = {
|
|
282
|
-
new: sql`AND (p.impressions IS NULL OR p.impressions = 0)`,
|
|
283
|
-
lost: sql`AND p.impressions > 0 AND c.impressions = 0`,
|
|
284
|
-
improving: sql`AND c.clicks > COALESCE(p.clicks, 0)`,
|
|
285
|
-
declining: sql`AND c.clicks < p.clicks AND p.clicks > 0`
|
|
286
|
-
};
|
|
287
|
-
function collapseWs(s) {
|
|
288
|
-
return s.replace(/\s+/g, " ").trim();
|
|
289
|
-
}
|
|
290
|
-
function joinAnd(parts) {
|
|
291
|
-
return sql.join(parts, sql` AND `);
|
|
292
|
-
}
|
|
293
|
-
function joinComma(parts) {
|
|
294
|
-
return sql.join(parts, sql`, `);
|
|
295
|
-
}
|
|
296
|
-
function orderByClause(state, prefix = "") {
|
|
297
|
-
if (state.orderBy) {
|
|
298
|
-
const safeCol = state.orderBy.column.replace(/\W/g, "");
|
|
299
|
-
const safeDir = state.orderBy.dir.toUpperCase() === "ASC" ? "ASC" : "DESC";
|
|
300
|
-
return sql.raw(`ORDER BY ${prefix}${safeCol} ${safeDir}`);
|
|
301
|
-
}
|
|
302
|
-
return sql.raw(`ORDER BY ${prefix}clicks DESC`);
|
|
303
|
-
}
|
|
304
|
-
function limitOffsetClause(state) {
|
|
305
|
-
const rowLimit = Math.max(0, Math.floor(Number(state.rowLimit ?? 100)));
|
|
306
|
-
const offset = state.startRow ? Math.max(0, Math.floor(Number(state.startRow))) : 0;
|
|
307
|
-
return sql.raw(offset > 0 ? `LIMIT ${rowLimit} OFFSET ${offset}` : `LIMIT ${rowLimit}`);
|
|
308
|
-
}
|
|
309
|
-
function aliasRaw(name) {
|
|
310
|
-
const safe = name.replace(/\W/g, "");
|
|
311
|
-
return sql.raw(`"${safe}"`);
|
|
312
|
-
}
|
|
313
|
-
function toInternalDimensionFilters(filters) {
|
|
314
|
-
return filters.map((filter) => ({
|
|
315
|
-
dimension: filter.dimension,
|
|
316
|
-
operator: filter.operator,
|
|
317
|
-
expression: filter.expression,
|
|
318
|
-
expression2: filter.expression2
|
|
319
|
-
}));
|
|
320
|
-
}
|
|
321
|
-
function toInternalMetricFilters(filters) {
|
|
322
|
-
return filters.map((filter) => ({
|
|
323
|
-
dimension: filter.metric,
|
|
324
|
-
operator: filter.operator,
|
|
325
|
-
expression: String(filter.expression),
|
|
326
|
-
expression2: filter.expression2 == null ? void 0 : String(filter.expression2)
|
|
327
|
-
}));
|
|
328
|
-
}
|
|
329
|
-
function topLevelFilters(plan) {
|
|
330
|
-
if (!plan.specialFilters.topLevel) return [];
|
|
331
|
-
return [{
|
|
332
|
-
dimension: "page",
|
|
333
|
-
operator: "topLevel",
|
|
334
|
-
expression: ""
|
|
335
|
-
}];
|
|
336
|
-
}
|
|
337
|
-
function logicalFilterToInternal(filter) {
|
|
338
|
-
return {
|
|
339
|
-
dimension: filter.dimension,
|
|
340
|
-
operator: filter.operator,
|
|
341
|
-
expression: filter.expression,
|
|
342
|
-
expression2: filter.expression2
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
function compileFilterTree(node, adapter, tableKey) {
|
|
346
|
-
if (!node) return void 0;
|
|
347
|
-
if (node.kind === "leaf") return adapter.dimensionPredicates([logicalFilterToInternal(node.filter)], tableKey)[0];
|
|
348
|
-
const childSqls = node.children.map((child) => compileFilterTree(child, adapter, tableKey)).filter((s) => s !== void 0);
|
|
349
|
-
if (childSqls.length === 0) return void 0;
|
|
350
|
-
if (childSqls.length === 1) return childSqls[0];
|
|
351
|
-
const sep = node.groupType === "or" ? sql` OR ` : sql` AND `;
|
|
352
|
-
return sql`(${sql.join(childSqls, sep)})`;
|
|
353
|
-
}
|
|
354
|
-
function buildScope(state, options) {
|
|
355
|
-
const { adapter, siteId } = options;
|
|
356
|
-
const plan = buildLogicalPlan(state, adapter.capabilities);
|
|
357
|
-
const tableKey = adapter.tableKeyForDataset(plan.dataset);
|
|
358
|
-
const dimFilters = toInternalDimensionFilters(plan.dimensionFilters);
|
|
359
|
-
const metricFilters = toInternalMetricFilters(plan.metricFilters);
|
|
360
|
-
const groupByDims = plan.groupByDimensions;
|
|
361
|
-
const hasDate = plan.hasDate;
|
|
362
|
-
const metrics = plan.metrics;
|
|
363
|
-
const wherePredicates = [];
|
|
364
|
-
if (adapter.siteIdColRef && siteId != null) wherePredicates.push(sql`${adapter.siteIdColRef(tableKey)} = ${siteId}`);
|
|
365
|
-
wherePredicates.push(sql`${adapter.dateColRef(tableKey)} >= ${plan.dateRange.startDate}`);
|
|
366
|
-
wherePredicates.push(sql`${adapter.dateColRef(tableKey)} <= ${plan.dateRange.endDate}`);
|
|
367
|
-
const dimSql = plan.dimensionFilterTree ? compileFilterTree(plan.dimensionFilterTree, adapter, tableKey) : void 0;
|
|
368
|
-
if (dimSql) wherePredicates.push(dimSql);
|
|
369
|
-
else if (!plan.dimensionFilterTree) wherePredicates.push(...adapter.dimensionPredicates(dimFilters, tableKey));
|
|
370
|
-
const tl = adapter.topLevelPredicate(topLevelFilters(plan), tableKey);
|
|
371
|
-
if (tl) wherePredicates.push(tl);
|
|
372
|
-
return {
|
|
373
|
-
plan,
|
|
374
|
-
tableKey,
|
|
375
|
-
groupByDims,
|
|
376
|
-
hasDate,
|
|
377
|
-
metrics,
|
|
378
|
-
wherePredicates,
|
|
379
|
-
having: adapter.havingPredicates(metricFilters, tableKey),
|
|
380
|
-
dimFilters,
|
|
381
|
-
startDate: plan.dateRange.startDate,
|
|
382
|
-
endDate: plan.dateRange.endDate
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
function buildComparisonPlan(current, previous, capabilities) {
|
|
386
|
-
return buildLogicalComparisonPlan(current, previous, capabilities);
|
|
387
|
-
}
|
|
388
|
-
function compileCollapsed(adapter, q) {
|
|
389
|
-
const c = adapter.compile(q);
|
|
390
|
-
return {
|
|
391
|
-
sql: collapseWs(c.sql),
|
|
392
|
-
params: c.params
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
function resolveToSQLOptimized(state, options) {
|
|
396
|
-
const { adapter } = options;
|
|
397
|
-
const { tableKey, groupByDims, hasDate, metrics, wherePredicates, having } = buildScope(state, options);
|
|
398
|
-
const table = adapter.tableRef(tableKey);
|
|
399
|
-
const schema = adapter.schema;
|
|
400
|
-
const cteSelect = [];
|
|
401
|
-
for (const d of groupByDims) {
|
|
402
|
-
const expr = adapter.dimExprSql(d, tableKey);
|
|
403
|
-
const colName = adapter.dimColumn(d, tableKey);
|
|
404
|
-
if (d === "page" || colName !== d) cteSelect.push(sql`${expr} as ${aliasRaw(d)}`);
|
|
405
|
-
else cteSelect.push(expr);
|
|
406
|
-
}
|
|
407
|
-
if (hasDate) cteSelect.push(adapter.dateColRef(tableKey));
|
|
408
|
-
const t = schema[tableKey];
|
|
409
|
-
cteSelect.push(sql`SUM(${t.clicks}) as clicks`);
|
|
410
|
-
cteSelect.push(sql`SUM(${t.impressions}) as impressions`);
|
|
411
|
-
cteSelect.push(sql`SUM(${t.sum_position}) as sum_position`);
|
|
412
|
-
const groupByExprs = groupByDims.map((d) => adapter.dimExprSql(d, tableKey));
|
|
413
|
-
if (hasDate) groupByExprs.push(adapter.dateColRef(tableKey));
|
|
414
|
-
const outerSelect = [];
|
|
415
|
-
for (const d of groupByDims) outerSelect.push(aliasRaw(d));
|
|
416
|
-
if (hasDate) outerSelect.push(sql.raw("date"));
|
|
417
|
-
const outerTotals = [];
|
|
418
|
-
for (const m of metrics) switch (m) {
|
|
419
|
-
case "clicks":
|
|
420
|
-
outerSelect.push(sql.raw("clicks"));
|
|
421
|
-
outerTotals.push(sql.raw("SUM(clicks) OVER() as totalClicks"));
|
|
422
|
-
break;
|
|
423
|
-
case "impressions":
|
|
424
|
-
outerSelect.push(sql.raw("impressions"));
|
|
425
|
-
outerTotals.push(sql.raw("SUM(impressions) OVER() as totalImpressions"));
|
|
426
|
-
break;
|
|
427
|
-
case "ctr":
|
|
428
|
-
outerSelect.push(sql.raw("CAST(clicks AS REAL) / NULLIF(impressions, 0) as ctr"));
|
|
429
|
-
outerTotals.push(sql.raw("CAST(SUM(clicks) OVER() AS REAL) / NULLIF(SUM(impressions) OVER(), 0) as totalCtr"));
|
|
430
|
-
break;
|
|
431
|
-
case "position":
|
|
432
|
-
outerSelect.push(sql.raw("sum_position / NULLIF(impressions, 0) + 1 as position"));
|
|
433
|
-
outerTotals.push(sql.raw("SUM(sum_position) OVER() / NULLIF(SUM(impressions) OVER(), 0) + 1 as totalPosition"));
|
|
434
|
-
break;
|
|
435
|
-
}
|
|
436
|
-
outerSelect.push(sql.raw("COUNT(*) OVER() as totalCount"));
|
|
437
|
-
for (const totalExpr of outerTotals) outerSelect.push(totalExpr);
|
|
438
|
-
let cte = wherePredicates.length > 0 ? sql`SELECT ${joinComma(cteSelect)} FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT ${joinComma(cteSelect)} FROM ${table}`;
|
|
439
|
-
if (groupByExprs.length > 0) cte = sql`${cte} GROUP BY ${joinComma(groupByExprs)}`;
|
|
440
|
-
if (having.length > 0) cte = sql`${cte} HAVING ${joinAnd(having)}`;
|
|
441
|
-
return compileCollapsed(adapter, sql`WITH aggregated AS (${cte}) SELECT ${joinComma(outerSelect)} FROM aggregated ${orderByClause(state)} ${limitOffsetClause(state)}`);
|
|
442
|
-
}
|
|
443
|
-
function resolveToSQL(state, options) {
|
|
444
|
-
const { adapter } = options;
|
|
445
|
-
const { tableKey, groupByDims, hasDate, metrics, wherePredicates, having } = buildScope(state, options);
|
|
446
|
-
const table = adapter.tableRef(tableKey);
|
|
447
|
-
const selectExprs = [];
|
|
448
|
-
for (const d of groupByDims) {
|
|
449
|
-
const expr = adapter.dimExprSql(d, tableKey);
|
|
450
|
-
const colName = adapter.dimColumn(d, tableKey);
|
|
451
|
-
if (d === "page" || colName !== d) selectExprs.push(sql`${expr} as ${aliasRaw(d)}`);
|
|
452
|
-
else selectExprs.push(expr);
|
|
453
|
-
}
|
|
454
|
-
if (hasDate) selectExprs.push(adapter.dateColRef(tableKey));
|
|
455
|
-
for (const m of metrics) selectExprs.push(sql`${adapter.metricSql(m, tableKey)} as ${aliasRaw(m)}`);
|
|
456
|
-
const groupByExprs = groupByDims.map((d) => adapter.dimExprSql(d, tableKey));
|
|
457
|
-
if (hasDate) groupByExprs.push(adapter.dateColRef(tableKey));
|
|
458
|
-
let body = wherePredicates.length > 0 ? sql`SELECT ${joinComma(selectExprs)} FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT ${joinComma(selectExprs)} FROM ${table}`;
|
|
459
|
-
if (groupByExprs.length > 0) body = sql`${body} GROUP BY ${joinComma(groupByExprs)}`;
|
|
460
|
-
if (having.length > 0) body = sql`${body} HAVING ${joinAnd(having)}`;
|
|
461
|
-
const mainQuery = sql`${body} ${orderByClause(state)} ${limitOffsetClause(state)}`;
|
|
462
|
-
let countQuery;
|
|
463
|
-
if (groupByExprs.length > 0) {
|
|
464
|
-
let inner = wherePredicates.length > 0 ? sql`SELECT ${joinComma(groupByExprs)} FROM ${table} WHERE ${joinAnd(wherePredicates)} GROUP BY ${joinComma(groupByExprs)}` : sql`SELECT ${joinComma(groupByExprs)} FROM ${table} GROUP BY ${joinComma(groupByExprs)}`;
|
|
465
|
-
if (having.length > 0) inner = sql`${inner} HAVING ${joinAnd(having)}`;
|
|
466
|
-
countQuery = sql`SELECT COUNT(*) as total FROM (${inner})`;
|
|
467
|
-
} else countQuery = wherePredicates.length > 0 ? sql`SELECT COUNT(*) as total FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT COUNT(*) as total FROM ${table}`;
|
|
468
|
-
const main = compileCollapsed(adapter, mainQuery);
|
|
469
|
-
const count = compileCollapsed(adapter, countQuery);
|
|
470
|
-
return {
|
|
471
|
-
sql: main.sql,
|
|
472
|
-
params: main.params,
|
|
473
|
-
countSql: count.sql,
|
|
474
|
-
countParams: count.params
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
function buildTotalsSql(state, options) {
|
|
478
|
-
const { adapter } = options;
|
|
479
|
-
const { tableKey, metrics, wherePredicates } = buildScope(state, options);
|
|
480
|
-
const table = adapter.tableRef(tableKey);
|
|
481
|
-
const selectExprs = metrics.map((m) => sql`${adapter.metricSql(m, tableKey)} as ${aliasRaw(m)}`);
|
|
482
|
-
return compileCollapsed(adapter, wherePredicates.length > 0 ? sql`SELECT ${joinComma(selectExprs)} FROM ${table} WHERE ${joinAnd(wherePredicates)}` : sql`SELECT ${joinComma(selectExprs)} FROM ${table}`);
|
|
483
|
-
}
|
|
484
|
-
function resolveComparisonSQL(current, previous, options, comparisonFilter) {
|
|
485
|
-
const { adapter, siteId } = options;
|
|
486
|
-
const comparisonPlan = buildComparisonPlan(current, previous, adapter.capabilities);
|
|
487
|
-
const currentScope = buildScope(current, options);
|
|
488
|
-
const previousScope = buildScope(previous, options);
|
|
489
|
-
const { tableKey, groupByDims, metrics, wherePredicates: currentWhere, having } = currentScope;
|
|
490
|
-
const table = adapter.tableRef(tableKey);
|
|
491
|
-
const dimSelectExprs = [];
|
|
492
|
-
for (const d of groupByDims) {
|
|
493
|
-
const expr = adapter.dimExprSql(d, tableKey);
|
|
494
|
-
const colName = adapter.dimColumn(d, tableKey);
|
|
495
|
-
if (d === "page" || colName !== d) dimSelectExprs.push(sql`${expr} as ${aliasRaw(d)}`);
|
|
496
|
-
else dimSelectExprs.push(expr);
|
|
497
|
-
}
|
|
498
|
-
const currentSelect = [...dimSelectExprs, ...metrics.map((m) => sql`${adapter.metricSql(m, tableKey)} as ${aliasRaw(m)}`)];
|
|
499
|
-
const prevSelect = [...dimSelectExprs, ...adapter.METRIC_NAMES.map((m) => sql`${adapter.metricSql(m, tableKey)} as ${aliasRaw(m)}`)];
|
|
500
|
-
const groupByExprs = groupByDims.map((d) => adapter.dimExprSql(d, tableKey));
|
|
501
|
-
const prevWhere = [];
|
|
502
|
-
if (adapter.siteIdColRef && siteId != null) prevWhere.push(sql`${adapter.siteIdColRef(tableKey)} = ${siteId}`);
|
|
503
|
-
if (previousScope.startDate) prevWhere.push(sql`${adapter.dateColRef(tableKey)} >= ${previousScope.startDate}`);
|
|
504
|
-
if (previousScope.endDate) prevWhere.push(sql`${adapter.dateColRef(tableKey)} <= ${previousScope.endDate}`);
|
|
505
|
-
const prevDimSql = comparisonPlan.current.dimensionFilterTree ? compileFilterTree(comparisonPlan.current.dimensionFilterTree, adapter, tableKey) : void 0;
|
|
506
|
-
if (prevDimSql) prevWhere.push(prevDimSql);
|
|
507
|
-
else if (!comparisonPlan.current.dimensionFilterTree) prevWhere.push(...adapter.dimensionPredicates(toInternalDimensionFilters(comparisonPlan.current.dimensionFilters), tableKey));
|
|
508
|
-
let currentCte = currentWhere.length > 0 ? sql`SELECT ${joinComma(currentSelect)} FROM ${table} WHERE ${joinAnd(currentWhere)}` : sql`SELECT ${joinComma(currentSelect)} FROM ${table}`;
|
|
509
|
-
if (groupByExprs.length > 0) currentCte = sql`${currentCte} GROUP BY ${joinComma(groupByExprs)}`;
|
|
510
|
-
if (having.length > 0) currentCte = sql`${currentCte} HAVING ${joinAnd(having)}`;
|
|
511
|
-
let previousCte = prevWhere.length > 0 ? sql`SELECT ${joinComma(prevSelect)} FROM ${table} WHERE ${joinAnd(prevWhere)}` : sql`SELECT ${joinComma(prevSelect)} FROM ${table}`;
|
|
512
|
-
if (groupByExprs.length > 0) previousCte = sql`${previousCte} GROUP BY ${joinComma(groupByExprs)}`;
|
|
513
|
-
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");
|
|
514
|
-
const filterClause = comparisonFilter ? COMPARISON_FILTER_SQL[comparisonFilter] : sql.raw("");
|
|
515
|
-
const orderSql = orderByClause(current, "c.");
|
|
516
|
-
const limitSql = limitOffsetClause(current);
|
|
517
|
-
const outerCurrentCols = [];
|
|
518
|
-
for (const d of groupByDims) {
|
|
519
|
-
const colName = d.replace(/\W/g, "");
|
|
520
|
-
outerCurrentCols.push(sql.raw(`c.${colName} as "${colName}"`));
|
|
521
|
-
}
|
|
522
|
-
outerCurrentCols.push(sql.raw("CAST(c.clicks AS DOUBLE) as \"clicks\""));
|
|
523
|
-
outerCurrentCols.push(sql.raw("CAST(c.impressions AS DOUBLE) as \"impressions\""));
|
|
524
|
-
outerCurrentCols.push(sql.raw("c.ctr as \"ctr\""));
|
|
525
|
-
outerCurrentCols.push(sql.raw("c.position as \"position\""));
|
|
526
|
-
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 LEFT JOIN previous p ON ${joinOn} WHERE 1=1 ${filterClause} ${orderSql} ${limitSql}`;
|
|
527
|
-
const firstGroupBy = groupByDims[0] ? groupByDims[0].replace(/\W/g, "") : "clicks";
|
|
528
|
-
const countInnerSelect = sql.raw(`c.${firstGroupBy}`);
|
|
529
|
-
const countQuery = sql`WITH current AS (${currentCte}), previous AS (${previousCte}) SELECT COUNT(*) as total FROM (SELECT ${countInnerSelect} FROM current c LEFT JOIN previous p ON ${joinOn} WHERE 1=1 ${filterClause})`;
|
|
530
|
-
const main = compileCollapsed(adapter, mainQuery);
|
|
531
|
-
const count = compileCollapsed(adapter, countQuery);
|
|
532
|
-
return {
|
|
533
|
-
sql: main.sql,
|
|
534
|
-
params: main.params,
|
|
535
|
-
countSql: count.sql,
|
|
536
|
-
countParams: count.params
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
function buildExtrasQueries(state, options) {
|
|
540
|
-
const { adapter, siteId } = options;
|
|
541
|
-
const plan = buildLogicalPlan(state, adapter.capabilities);
|
|
542
|
-
const dims = plan.groupByDimensions;
|
|
543
|
-
const extras = [];
|
|
544
|
-
if (!dims.includes("queryCanonical")) return extras;
|
|
545
|
-
const keywordsKey = adapter.tableKeyForDataset("keywords");
|
|
546
|
-
const t = adapter.schema[keywordsKey];
|
|
547
|
-
const table = adapter.tableRef(keywordsKey);
|
|
548
|
-
const whereParts = [];
|
|
549
|
-
if (adapter.siteIdColRef && siteId != null) whereParts.push(sql`${adapter.siteIdColRef(keywordsKey)} = ${siteId}`);
|
|
550
|
-
whereParts.push(sql`${adapter.dateColRef(keywordsKey)} >= ${plan.dateRange.startDate}`);
|
|
551
|
-
whereParts.push(sql`${adapter.dateColRef(keywordsKey)} <= ${plan.dateRange.endDate}`);
|
|
552
|
-
const whereExpr = whereParts.length > 0 ? sql`WHERE ${joinAnd(whereParts)}` : sql``;
|
|
553
|
-
const outerQueryCol = sql.raw("query");
|
|
554
|
-
const compiled = compileCollapsed(adapter, sql`WITH per_variant AS (SELECT ${t.query_canonical} as joinKey, ${t.query} as query, SUM(${t.clicks}) as clicks, SUM(${t.impressions}) as impressions, SUM(${t.sum_position}) as sum_pos, ROW_NUMBER() OVER (PARTITION BY ${t.query_canonical} ORDER BY SUM(${t.clicks}) DESC) as rn, COUNT(*) OVER (PARTITION BY ${t.query_canonical}) as variantCount FROM ${table} ${whereExpr} GROUP BY ${t.query_canonical}, ${t.query}) SELECT joinKey, MAX(variantCount) as variantCount, MAX(CASE WHEN rn = 1 THEN ${outerQueryCol} END) as canonicalName, GROUP_CONCAT(CASE WHEN rn <= 10 THEN ${outerQueryCol} || ':::' || clicks || ':::' || impressions || ':::' || CAST(ROUND(CAST(sum_pos AS REAL) / NULLIF(impressions, 0) + 1, 1) AS TEXT) END, '||') as variants FROM per_variant GROUP BY joinKey`);
|
|
555
|
-
extras.push({
|
|
556
|
-
key: "canonicalExtras",
|
|
557
|
-
sql: compiled.sql,
|
|
558
|
-
params: compiled.params
|
|
559
|
-
});
|
|
560
|
-
return extras;
|
|
561
|
-
}
|
|
562
|
-
function mergeExtras(rows, extrasResults) {
|
|
563
|
-
if (extrasResults.length === 0) return rows;
|
|
564
|
-
const lookups = [];
|
|
565
|
-
for (const { key, results } of extrasResults) {
|
|
566
|
-
if (key === "canonicalExtras") {
|
|
567
|
-
const variantCountMap = /* @__PURE__ */ new Map();
|
|
568
|
-
const variantsMap = /* @__PURE__ */ new Map();
|
|
569
|
-
const canonicalNameMap = /* @__PURE__ */ new Map();
|
|
570
|
-
for (const r of results) {
|
|
571
|
-
const jk = String(r.joinKey);
|
|
572
|
-
variantCountMap.set(jk, r.variantCount);
|
|
573
|
-
canonicalNameMap.set(jk, r.canonicalName);
|
|
574
|
-
const raw = r.variants;
|
|
575
|
-
variantsMap.set(jk, typeof raw === "string" ? raw.split("||").filter(Boolean).map((v) => {
|
|
576
|
-
const parts = v.split(":::");
|
|
577
|
-
return {
|
|
578
|
-
query: parts[0],
|
|
579
|
-
clicks: Number(parts[1] || 0),
|
|
580
|
-
impressions: Number(parts[2] || 0),
|
|
581
|
-
position: Number(parts[3] || 0)
|
|
582
|
-
};
|
|
583
|
-
}) : []);
|
|
584
|
-
}
|
|
585
|
-
lookups.push({
|
|
586
|
-
key: "variantCount",
|
|
587
|
-
map: variantCountMap
|
|
588
|
-
});
|
|
589
|
-
lookups.push({
|
|
590
|
-
key: "variants",
|
|
591
|
-
map: variantsMap
|
|
592
|
-
});
|
|
593
|
-
lookups.push({
|
|
594
|
-
key: "canonicalName",
|
|
595
|
-
map: canonicalNameMap
|
|
596
|
-
});
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
const filtered = results.filter((r) => r.rn === void 0 || r.rn === 1);
|
|
600
|
-
const map = /* @__PURE__ */ new Map();
|
|
601
|
-
for (const r of filtered) {
|
|
602
|
-
let val = r[key];
|
|
603
|
-
if (key === "variants" && typeof val === "string") val = val.split("||").filter(Boolean).map((v) => {
|
|
604
|
-
const parts = v.split(":::");
|
|
605
|
-
return {
|
|
606
|
-
query: parts[0],
|
|
607
|
-
clicks: Number(parts[1] || 0),
|
|
608
|
-
impressions: Number(parts[2] || 0),
|
|
609
|
-
position: Number(parts[3] || 0)
|
|
610
|
-
};
|
|
611
|
-
});
|
|
612
|
-
map.set(String(r.joinKey), val);
|
|
613
|
-
}
|
|
614
|
-
lookups.push({
|
|
615
|
-
key,
|
|
616
|
-
map
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
return rows.map((row) => {
|
|
620
|
-
const enriched = { ...row };
|
|
621
|
-
for (const { key, map } of lookups) {
|
|
622
|
-
let joinValue;
|
|
623
|
-
if (key === "variantCount" || key === "variants" || key === "canonicalName") joinValue = String(row.queryCanonical ?? row.query_canonical ?? "");
|
|
624
|
-
enriched[key] = (joinValue && map.get(joinValue)) ?? (key === "variants" ? [] : null);
|
|
625
|
-
if (key === "canonicalName" && enriched[key]) enriched.queryCanonical = enriched[key];
|
|
626
|
-
}
|
|
627
|
-
return enriched;
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
4
|
function createSqlQuerySource(options) {
|
|
631
5
|
const { name, adapter, execute, siteId, extraCapabilities } = options;
|
|
632
6
|
return {
|
|
@@ -647,22 +21,6 @@ function createSqlQuerySource(options) {
|
|
|
647
21
|
}
|
|
648
22
|
};
|
|
649
23
|
}
|
|
650
|
-
const pgDialect = new PgDialect();
|
|
651
|
-
const sqliteDialect = new SQLiteAsyncDialect();
|
|
652
|
-
function compilePg(query) {
|
|
653
|
-
const compiled = pgDialect.sqlToQuery(query);
|
|
654
|
-
return {
|
|
655
|
-
sql: compiled.sql,
|
|
656
|
-
params: compiled.params
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
function compileSqlite(query) {
|
|
660
|
-
const compiled = sqliteDialect.sqlToQuery(query);
|
|
661
|
-
return {
|
|
662
|
-
sql: compiled.sql,
|
|
663
|
-
params: compiled.params
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
24
|
function collectInternalFilters(filter) {
|
|
667
25
|
if (!filter || !("_filters" in filter)) return [];
|
|
668
26
|
const flat = filter._filters;
|
|
@@ -717,142 +75,6 @@ function matchesMetricFilter(row, filter) {
|
|
|
717
75
|
function matchesTopLevelPage(row) {
|
|
718
76
|
return (normalizeUrl(dimensionValue(row, "page")).match(/\//g)?.length ?? 0) <= 1;
|
|
719
77
|
}
|
|
720
|
-
function metricCols() {
|
|
721
|
-
return {
|
|
722
|
-
clicks: integer("clicks").notNull(),
|
|
723
|
-
impressions: integer("impressions").notNull(),
|
|
724
|
-
sum_position: doublePrecision("sum_position").notNull()
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
const dateCol = () => date("date").notNull();
|
|
728
|
-
const drizzleSchema = {
|
|
729
|
-
pages: pgTable("pages", {
|
|
730
|
-
url: varchar("url").notNull(),
|
|
731
|
-
date: dateCol(),
|
|
732
|
-
...metricCols()
|
|
733
|
-
}),
|
|
734
|
-
keywords: pgTable("keywords", {
|
|
735
|
-
query: varchar("query").notNull(),
|
|
736
|
-
query_canonical: varchar("query_canonical"),
|
|
737
|
-
date: dateCol(),
|
|
738
|
-
...metricCols()
|
|
739
|
-
}),
|
|
740
|
-
countries: pgTable("countries", {
|
|
741
|
-
country: varchar("country").notNull(),
|
|
742
|
-
date: dateCol(),
|
|
743
|
-
...metricCols()
|
|
744
|
-
}),
|
|
745
|
-
devices: pgTable("devices", {
|
|
746
|
-
device: varchar("device").notNull(),
|
|
747
|
-
date: dateCol(),
|
|
748
|
-
...metricCols()
|
|
749
|
-
}),
|
|
750
|
-
page_keywords: pgTable("page_keywords", {
|
|
751
|
-
url: varchar("url").notNull(),
|
|
752
|
-
query: varchar("query").notNull(),
|
|
753
|
-
query_canonical: varchar("query_canonical"),
|
|
754
|
-
date: dateCol(),
|
|
755
|
-
...metricCols()
|
|
756
|
-
}),
|
|
757
|
-
search_appearance: pgTable("search_appearance", {
|
|
758
|
-
searchAppearance: varchar("searchAppearance").notNull(),
|
|
759
|
-
date: dateCol(),
|
|
760
|
-
...metricCols()
|
|
761
|
-
})
|
|
762
|
-
};
|
|
763
|
-
const TABLE_METADATA = {
|
|
764
|
-
pages: {
|
|
765
|
-
sortKey: ["date", "url"],
|
|
766
|
-
version: 1
|
|
767
|
-
},
|
|
768
|
-
keywords: {
|
|
769
|
-
sortKey: ["date", "query"],
|
|
770
|
-
version: 2
|
|
771
|
-
},
|
|
772
|
-
countries: {
|
|
773
|
-
sortKey: ["date", "country"],
|
|
774
|
-
version: 1
|
|
775
|
-
},
|
|
776
|
-
devices: {
|
|
777
|
-
sortKey: ["date", "device"],
|
|
778
|
-
version: 1
|
|
779
|
-
},
|
|
780
|
-
page_keywords: {
|
|
781
|
-
sortKey: [
|
|
782
|
-
"date",
|
|
783
|
-
"url",
|
|
784
|
-
"query"
|
|
785
|
-
],
|
|
786
|
-
version: 2
|
|
787
|
-
},
|
|
788
|
-
search_appearance: {
|
|
789
|
-
sortKey: ["date", "searchAppearance"],
|
|
790
|
-
version: 1
|
|
791
|
-
}
|
|
792
|
-
};
|
|
793
|
-
const PG_BASE_CONFIG = {
|
|
794
|
-
schema: drizzleSchema,
|
|
795
|
-
datasetToTableKey: {
|
|
796
|
-
pages: "pages",
|
|
797
|
-
keywords: "keywords",
|
|
798
|
-
page_keywords: "page_keywords",
|
|
799
|
-
countries: "countries",
|
|
800
|
-
devices: "devices",
|
|
801
|
-
search_appearance: "search_appearance"
|
|
802
|
-
},
|
|
803
|
-
metricCast: "DOUBLE",
|
|
804
|
-
regexPredicate: (expr, pattern, negate) => negate ? sql`NOT regexp_matches(${expr}, ${pattern})` : sql`regexp_matches(${expr}, ${pattern})`,
|
|
805
|
-
urlToPathExpr: (col) => `CASE WHEN ${col} LIKE 'http%' THEN COALESCE(NULLIF(regexp_replace(${col}, '^https?://[^/]+', ''), ''), '/') ELSE ${col} END`,
|
|
806
|
-
includeSiteId: false,
|
|
807
|
-
compile: compilePg,
|
|
808
|
-
capabilities: {
|
|
809
|
-
regex: true,
|
|
810
|
-
comparisonJoin: true,
|
|
811
|
-
windowTotals: true
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
const pgResolverAdapter = createResolverAdapter({
|
|
815
|
-
...PG_BASE_CONFIG,
|
|
816
|
-
tableLabel: "pg-resolver-adapter"
|
|
817
|
-
});
|
|
818
|
-
function createParquetResolverAdapter() {
|
|
819
|
-
return createResolverAdapter({
|
|
820
|
-
...PG_BASE_CONFIG,
|
|
821
|
-
tableLabel: "parquet-resolver-adapter",
|
|
822
|
-
tableRef: (tk) => sql.raw(`read_parquet({{FILES}}, union_by_name = true) AS "${tk}"`)
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
function pgSqlTypeToColumnType(sqlType) {
|
|
826
|
-
const t = sqlType.toLowerCase();
|
|
827
|
-
if (t.startsWith("varchar") || t === "text" || t.startsWith("char")) return "VARCHAR";
|
|
828
|
-
if (t === "date" || t.startsWith("timestamp")) return "DATE";
|
|
829
|
-
if (t.startsWith("double") || t === "real" || t.startsWith("numeric") || t.startsWith("decimal")) return "DOUBLE";
|
|
830
|
-
if (t === "bigint" || t === "int8") return "BIGINT";
|
|
831
|
-
if (t === "integer" || t === "int" || t === "int4" || t === "smallint" || t === "int2") return "INTEGER";
|
|
832
|
-
throw new Error(`unmapped pg type '${sqlType}' — extend pgSqlTypeToColumnType in @gscdump/engine/schema`);
|
|
833
|
-
}
|
|
834
|
-
function tableSchemaFrom(tableName) {
|
|
835
|
-
const columns = getTableConfig(drizzleSchema[tableName]).columns.map((col) => ({
|
|
836
|
-
name: col.name,
|
|
837
|
-
type: pgSqlTypeToColumnType(col.getSQLType()),
|
|
838
|
-
nullable: !col.notNull
|
|
839
|
-
}));
|
|
840
|
-
const meta = TABLE_METADATA[tableName];
|
|
841
|
-
return {
|
|
842
|
-
name: tableName,
|
|
843
|
-
columns,
|
|
844
|
-
sortKey: meta.sortKey,
|
|
845
|
-
version: meta.version
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
const SCHEMAS = Object.fromEntries([
|
|
849
|
-
"pages",
|
|
850
|
-
"keywords",
|
|
851
|
-
"countries",
|
|
852
|
-
"devices",
|
|
853
|
-
"page_keywords",
|
|
854
|
-
"search_appearance"
|
|
855
|
-
].map((t) => [t, tableSchemaFrom(t)]));
|
|
856
78
|
function assertSchemaInSync(options) {
|
|
857
79
|
const { label, schema, tableKeyToName, mode } = options;
|
|
858
80
|
for (const [key, table] of Object.entries(schema)) {
|